From 89b582ecc6ae5fee6d761a4300abf4b32ece735a Mon Sep 17 00:00:00 2001 From: martha Date: Mon, 16 Dec 2024 12:45:24 -0500 Subject: [PATCH 01/13] Implement Table Actions pattern and default columns --- src/components/elements/CommonMenuButton.tsx | 2 + .../elements/table/GenericTable.stories.tsx | 1 - .../elements/table/GenericTable.tsx | 13 +- .../elements/table/TableRowActions.tsx | 61 +++- .../table/tableActions/tableRowActionUtil.ts | 74 +++++ .../components/services/ServiceTypeTable.tsx | 25 +- .../pages/ClientAssessmentsPage.tsx | 43 +-- src/modules/assessments/util.tsx | 44 ++- .../components/AssignServiceButton.tsx | 1 + .../components/BulkServicesTable.tsx | 148 +++++---- .../caseNotes/components/ClientCaseNotes.tsx | 37 ++- .../components/EnrollmentCaseNotes.tsx | 36 ++- .../components/ClientEnrollmentCard.tsx | 19 +- .../components/ClientSearchResultCard.tsx | 2 +- .../components/ClientFilesPage.tsx | 79 ++--- .../components/GenericTableWithData.tsx | 1 + .../components/EnrollmentAssessmentsTable.tsx | 29 +- .../components/HouseholdAssessmentsTable.tsx | 41 ++- .../pages/ClientEnrollmentsPage.tsx | 90 +++--- .../pages/EnrollmentCeAssessmentsPage.tsx | 97 +++--- .../pages/EnrollmentCeEventsPage.tsx | 33 +- .../EnrollmentCurrentLivingSituationsPage.tsx | 28 +- .../components/DateWithRelativeTooltip.tsx | 59 ++++ .../hmis/components/RelativeDateDisplay.tsx | 26 +- src/modules/hmis/hmisUtil.ts | 22 +- .../components/HouseholdMemberTable.tsx | 31 -- .../components/AllProjectsPage.tsx | 283 ++++++++++-------- .../components/ProjectAssessments.tsx | 72 ++--- .../ProjectCurrentLivingSituations.tsx | 82 +++-- .../tables/ProjectClientEnrollmentsTable.tsx | 139 +++++---- .../ProjectExternalSubmissionsTable.tsx | 29 +- .../tables/ProjectHouseholdsTable.tsx | 76 +++-- .../search/components/ClientSearch.tsx | 79 +---- .../components/ClientServicesPage.tsx | 65 ++-- .../components/EnrollmentServicesPage.tsx | 52 ++-- .../components/ProjectServicesTable.tsx | 63 ++-- src/modules/services/serviceColumns.tsx | 25 +- 37 files changed, 1130 insertions(+), 877 deletions(-) create mode 100644 src/components/elements/table/tableActions/tableRowActionUtil.ts create mode 100644 src/modules/hmis/components/DateWithRelativeTooltip.tsx diff --git a/src/components/elements/CommonMenuButton.tsx b/src/components/elements/CommonMenuButton.tsx index 4828eaf52..3875c3555 100644 --- a/src/components/elements/CommonMenuButton.tsx +++ b/src/components/elements/CommonMenuButton.tsx @@ -13,6 +13,7 @@ import { To } from 'react-router-dom'; import RouterLink from './RouterLink'; import { MoreMenuIcon } from './SemanticIcons'; +import { LocationState } from '@/routes/routeUtil'; export type CommonMenuItem = { key: string; @@ -22,6 +23,7 @@ export type CommonMenuItem = { divider?: boolean; disabled?: boolean; ariaLabel?: string; + linkState?: LocationState; }; interface Props { diff --git a/src/components/elements/table/GenericTable.stories.tsx b/src/components/elements/table/GenericTable.stories.tsx index 6a437eba4..c7116e42c 100644 --- a/src/components/elements/table/GenericTable.stories.tsx +++ b/src/components/elements/table/GenericTable.stories.tsx @@ -30,7 +30,6 @@ const Template = ); const clientColumns = [ - CLIENT_COLUMNS.id, CLIENT_COLUMNS.first, CLIENT_COLUMNS.last, CLIENT_COLUMNS.ssn, diff --git a/src/components/elements/table/GenericTable.tsx b/src/components/elements/table/GenericTable.tsx index e1d0daf1c..d0267365a 100644 --- a/src/components/elements/table/GenericTable.tsx +++ b/src/components/elements/table/GenericTable.tsx @@ -44,16 +44,15 @@ import { import TableRowActions, { TableRowActionsType, } from '@/components/elements/table/TableRowActions'; -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; + loadingRegardlessOfData?: boolean; // needed for correctly updating AssignBulkService button loadingVariant?: 'circular' | 'linear'; tablePaginationProps?: TablePaginationProps; tableContainerProps?: TableContainerProps; @@ -74,12 +73,11 @@ export interface Props { filterToolbar?: ReactNode; noData?: ReactNode; renderRow?: (row: T, columnKeys: string[]) => ReactNode; - condensed?: boolean; // if overrideTableBody is true, GenericTable doesn't render a `tbody` element. // This should only be used by tables that take over rendering using renderRow and render a `tbody` within their custom render fn overrideTableBody?: boolean; injectBelowRows?: ReactNode; // component to inject below all rendered rows, above footer - getTableRowActions?: (record: T) => TableRowActionsType; + getTableRowActions?: (record: T, loading?: boolean) => TableRowActionsType; getRowAccessibleName?: (row: T) => string; } @@ -118,6 +116,7 @@ const GenericTable = ({ columns: columnProp, paginated = false, loading = false, + loadingRegardlessOfData = false, vertical = false, renderVerticalHeaderCell, tablePaginationProps, @@ -135,8 +134,6 @@ const GenericTable = ({ renderRow, noData = 'No data', loadingVariant = 'circular', - condensed = false, - rowLinkState, overrideTableBody = false, injectBelowRows, getTableRowActions, @@ -435,7 +432,6 @@ const GenericTable = ({ {isLinked ? ( ({ height: '100%', alignItems: 'center', px: 2, - py: condensed ? 1 : 2, + py: 2, }} > {renderCellContents(row, render)} @@ -477,6 +473,7 @@ const GenericTable = ({ : row.id } getActions={getTableRowActions} + loading={loadingRegardlessOfData} /> )} diff --git a/src/components/elements/table/TableRowActions.tsx b/src/components/elements/table/TableRowActions.tsx index 4c1931b58..f0d175289 100644 --- a/src/components/elements/table/TableRowActions.tsx +++ b/src/components/elements/table/TableRowActions.tsx @@ -1,23 +1,25 @@ -import { Stack } from '@mui/material'; -import { useMemo } from 'react'; +import { Button, Stack } from '@mui/material'; +import React, { ReactNode, useMemo } from 'react'; import ButtonLink from '../ButtonLink'; import CommonMenuButton, { CommonMenuItem } from '../CommonMenuButton'; export type TableRowActionsType = { - primaryAction?: CommonMenuItem; + primaryAction?: CommonMenuItem | ReactNode; secondaryActions?: CommonMenuItem[]; }; interface TableRowActionsProps { record: T; recordName?: string; - getActions: (record: T) => TableRowActionsType; + loading?: boolean; + getActions: (record: T, loading?: boolean) => TableRowActionsType; } const TableRowActions = ({ record, recordName, getActions, + loading, }: TableRowActionsProps) => { const accessibleName = useMemo( () => recordName || record.id, @@ -25,27 +27,62 @@ const TableRowActions = ({ ); const { primaryAction, secondaryActions } = useMemo( - () => getActions(record), - [getActions, record] + () => getActions(record, loading), + [getActions, loading, record] + ); + + const { primaryActionNode, primaryActionTypesafe } = useMemo(() => { + return React.isValidElement(primaryAction) + ? { primaryActionNode: primaryAction } + : { primaryActionTypesafe: primaryAction as CommonMenuItem }; + }, [primaryAction]); + + const secondariesWithAria = useMemo( + () => + secondaryActions?.map((s) => { + return { + ...s, + ariaLabel: s.ariaLabel || `${s.title}, ${recordName}`, + }; + }), + [secondaryActions, recordName] ); return ( - {!!primaryAction && ( + {primaryActionNode} + {!!primaryActionTypesafe && !!primaryActionTypesafe.to && ( - {primaryAction.title} + {primaryActionTypesafe.title} )} - {!!secondaryActions && secondaryActions.length > 0 && ( + {!!primaryActionTypesafe && !!primaryActionTypesafe.onClick && ( + + )} + {!!secondariesWithAria && secondariesWithAria.length > 0 && ( { + return { + title: 'View Client', + key: 'client', + ariaLabel: `View Client, ${clientBriefName(client)}`, + to: generateSafePath(ClientDashboardRoutes.PROFILE, { + clientId: client.id, + }), + }; +}; + +export const getViewEnrollmentAction = ( + enrollment: Pick, + client: Pick | ClientNameFragment +) => { + return { + title: 'View Enrollment', + key: 'enrollment', + ariaLabel: `View Enrollment, ${client.hasOwnProperty('firstName') ? clientBriefName(client as ClientNameFragment) : ''} ${entryExitRange(enrollment)}`, + to: generateSafePath(EnrollmentDashboardRoutes.ENROLLMENT_OVERVIEW, { + clientId: client.id, + enrollmentId: enrollment.id, + }), + }; +}; + +export const getViewAssessmentAction = ( + assessment: AssessmentFieldsFragment, + clientId: string, + enrollmentId: string +) => { + return { + title: 'View Assessment', + key: 'assessment', + ariaLabel: `View Assessment, ${assessmentDescription(assessment)}`, + to: generateAssessmentPath(assessment, clientId, enrollmentId), + }; +}; + +export const getViewServiceAction = ( + 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/modules/admin/components/services/ServiceTypeTable.tsx b/src/modules/admin/components/services/ServiceTypeTable.tsx index 2e0db0ce1..5b6c3f081 100644 --- a/src/modules/admin/components/services/ServiceTypeTable.tsx +++ b/src/modules/admin/components/services/ServiceTypeTable.tsx @@ -2,6 +2,7 @@ import { Chip } from '@mui/material'; 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 +12,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', @@ -42,6 +42,18 @@ const columns: ColumnDef[] = [ }, ]; +const getTableRowActions = (row: ServiceTypeConfigFieldsFragment) => { + return { + primaryAction: { + title: 'View Service Type', + key: 'service type', + to: generateSafePath(AdminDashboardRoutes.CONFIGURE_SERVICE_TYPE, { + serviceTypeId: row.id, + }), + }, + }; +}; + const ServiceTypeTable = () => { const filters = useFilters({ type: 'ServiceTypeFilterOptions', @@ -57,17 +69,14 @@ const ServiceTypeTable = () => { > queryVariables={{}} queryDocument={GetServiceTypesDocument} - columns={columns} + columns={COLUMNS} + getTableRowActions={getTableRowActions} + getRowAccessibleName={(record) => getServiceTypeForDisplay(record)} 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/assessments/components/pages/ClientAssessmentsPage.tsx b/src/modules/assessments/components/pages/ClientAssessmentsPage.tsx index 95dd8031d..880de8e97 100644 --- a/src/modules/assessments/components/pages/ClientAssessmentsPage.tsx +++ b/src/modules/assessments/components/pages/ClientAssessmentsPage.tsx @@ -1,17 +1,13 @@ import { Paper } from '@mui/material'; import { useCallback } from 'react'; +import { getViewAssessmentAction } from '@/components/elements/table/tableActions/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 +17,36 @@ import { GetClientAssessmentsQueryVariables, } from '@/types/gqlTypes'; -const columns: ColumnDef[] = [ - ASSESSMENT_COLUMNS.linkedType, - { - header: 'Assessment Date', - render: (a) => , - ariaLabel: (row) => assessmentDescription(row), - }, +const COLUMNS: ColumnDef[] = [ + ASSESSMENT_COLUMNS.date, { header: 'Project Name', render: (row) => row.enrollment.projectName, }, - ASSESSMENT_ENROLLMENT_COLUMNS.period, + ASSESSMENT_COLUMNS.type, + ASSESSMENT_COLUMNS.lastUpdated, ]; const ClientAssessmentsPage = () => { const { clientId } = useSafeParams() as { clientId: string }; - const rowLinkTo = useCallback( - (record: ClientAssessmentType) => assessmentRowLinkTo(record, clientId), - [clientId] - ); - const filters = useFilters({ type: 'AssessmentFilterOptions', }); + const getTableRowActions = useCallback( + (record: ClientAssessmentType) => { + return { + primaryAction: getViewAssessmentAction( + record, + clientId, + record.enrollment.id + ), + }; + }, + [clientId] + ); + return ( <> @@ -59,8 +59,9 @@ const ClientAssessmentsPage = () => { filters={filters} queryVariables={{ id: clientId }} queryDocument={GetClientAssessmentsDocument} - rowLinkTo={rowLinkTo} - columns={columns} + getTableRowActions={getTableRowActions} + getRowAccessibleName={(row) => assessmentDescription(row)} + columns={COLUMNS} pagePath='client.assessments' fetchPolicy='cache-and-network' noData='No assessments' diff --git a/src/modules/assessments/util.tsx b/src/modules/assessments/util.tsx index 885eccdc5..22adcee39 100644 --- a/src/modules/assessments/util.tsx +++ b/src/modules/assessments/util.tsx @@ -1,10 +1,10 @@ import { AlwaysPresentLocalConstants } from '../form/util/formUtil'; +import RelativeDateDisplay from '../hmis/components/RelativeDateDisplay'; import { ClientAssessmentType } from './assessmentTypes'; 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 2a10db21d..d9bb1382a 100644 --- a/src/modules/bulkServices/components/AssignServiceButton.tsx +++ b/src/modules/bulkServices/components/AssignServiceButton.tsx @@ -125,6 +125,7 @@ const AssignServiceButton: React.FC = ({ startIcon={startIcon} fullWidth variant={isAssignedOnDate ? 'contained' : 'gray'} + aria-label={`${buttonText}, ${clientBriefName(client)}`} > {buttonText} diff --git a/src/modules/bulkServices/components/BulkServicesTable.tsx b/src/modules/bulkServices/components/BulkServicesTable.tsx index 715505d9e..d7ff11649 100644 --- a/src/modules/bulkServices/components/BulkServicesTable.tsx +++ b/src/modules/bulkServices/components/BulkServicesTable.tsx @@ -3,26 +3,29 @@ 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 { + getViewClientAction, + getViewEnrollmentAction, +} from '@/components/elements/table/tableActions/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,83 +62,74 @@ const BulkServicesTable: React.FC = ({ [cocCode, projectId, serviceDate, serviceTypeId] ); - const getColumnDefs = useCallback( - (_rows: RowType[], loading?: boolean) => { - const notEnrolledText = ( - - Not enrolled on {formatDateForDisplay(serviceDate, 'M/d')} - - ); - return [ - { ...CLIENT_COLUMNS.linkedId }, - CLIENT_COLUMNS.first, - CLIENT_COLUMNS.last, - CLIENT_COLUMNS.dobAge, - { - header: 'Entry Date', - render: (row: RowType) => { - if (!row.activeEnrollment) return notEnrolledText; + const [canViewDob] = useHasRootPermissions(['canViewDob']); - return ( - - {parseAndFormatDate(row.activeEnrollment.entryDate)} - - ); - }, - }, - { - header: `Last ${serviceTypeName} Date`, - render: (row: RowType) => { - if (!row.activeEnrollment) return notEnrolledText; + const columns = useMemo(() => { + const notEnrolledText = ( + + Not enrolled on {formatDateForDisplay(serviceDate, 'M/d')} + + ); + return [ + CLIENT_COLUMNS.name, + ...(canViewDob ? [CLIENT_COLUMNS.dobAge] : []), + { + header: 'Entry Date', + render: (row: RowType) => { + if (!row.activeEnrollment) return notEnrolledText; - const noService = ( - - No Previous {serviceTypeName} - - ); - if (!row.activeEnrollment.lastServiceDate) { - return noService; - } - const dt = parseHmisDateString( - row.activeEnrollment.lastServiceDate - ); - if (!dt) return noService; - const relative = formatRelativeDate(dt); - const formatted = formatDateForDisplay(dt); - return `${relative} (${formatted})`; - }, + return parseAndFormatDate(row.activeEnrollment.entryDate); }, - { - header: `Assign ${serviceTypeName} for ${formatDateForDisplay( - serviceDate - )}`, - width: '180px', - render: (row: RowType) => { - return ( - - ); - }, + }, + { + header: `Last ${serviceTypeName} Date`, + render: (row: RowType) => { + if (!row.activeEnrollment) return notEnrolledText; + + const noService = ( + + No Previous {serviceTypeName} + + ); + if (!row.activeEnrollment.lastServiceDate) { + return noService; + } + const dt = parseHmisDateString(row.activeEnrollment.lastServiceDate); + if (!dt) return noService; + const relative = formatRelativeDate(dt); + const formatted = formatDateForDisplay(dt); + return `${relative} (${formatted})`; }, - ] as ColumnDef[]; + }, + ] as ColumnDef[]; + }, [canViewDob, serviceDate, serviceTypeName]); + + const getTableRowActions = useCallback( + (record: RowType, loading?: boolean) => { + return { + primaryAction: ( + + ), + secondaryActions: [ + getViewClientAction(record), + ...(record.activeEnrollment + ? [getViewEnrollmentAction(record.activeEnrollment, record)] + : []), + ], + }; }, - [serviceDate, serviceTypeName, mutationQueryVariables, anyRowsSelected] + [anyRowsSelected, mutationQueryVariables, serviceTypeName] ); const defaultFilterValues = useMemo(() => { @@ -174,7 +168,9 @@ const BulkServicesTable: React.FC = ({ onChangeSelectedRowIds={(rows) => setAnyRowsSelected(rows.length > 0)} queryDocument={BulkServicesClientSearchDocument} pagePath='clientSearch' - getColumnDefs={getColumnDefs} + columns={columns} + getTableRowActions={getTableRowActions} + getRowAccessibleName={(record: RowType) => clientBriefName(record)} recordType='Client' // TODO: add user-facing filter options for enrolled clients and bed night date. No filter options for now. defaultFilterValues={defaultFilterValues} diff --git a/src/modules/caseNotes/components/ClientCaseNotes.tsx b/src/modules/caseNotes/components/ClientCaseNotes.tsx index 096608ffe..965b35c61 100644 --- a/src/modules/caseNotes/components/ClientCaseNotes.tsx +++ b/src/modules/caseNotes/components/ClientCaseNotes.tsx @@ -1,5 +1,6 @@ import { Paper } from '@mui/material'; +import { useCallback } from 'react'; import { CASE_NOTE_COLUMNS } from './EnrollmentCaseNotes'; import { ColumnDef } from '@/components/elements/table/types'; import PageTitle from '@/components/layout/PageTitle'; @@ -7,33 +8,27 @@ 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[] = [ +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) => ( - - ), - }, + CASE_NOTE_COLUMNS.LastUpdated, + CASE_NOTE_COLUMNS.NoteContentPreview, ]; const ClientCaseNotes = () => { @@ -49,6 +44,19 @@ const ClientCaseNotes = () => { maxWidth: 'sm', }); + const getTableRowActions = useCallback( + (record: Row) => { + return { + primaryAction: { + title: 'View Case Note', + key: 'case note', + onClick: () => onSelectRecord(record), + }, + }; + }, + [onSelectRecord] + ); + if (!clientId) return ; return ( @@ -62,11 +70,14 @@ const ClientCaseNotes = () => { > queryVariables={{ id: clientId }} queryDocument={GetClientCaseNotesDocument} - columns={columns} + columns={COLUMNS} + getTableRowActions={getTableRowActions} + getRowAccessibleName={(row) => + `${parseAndFormatDate(row.informationDate)} at ${row.enrollment.projectName}` + } 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 0225737b1..9023c0ee7 100644 --- a/src/modules/caseNotes/components/EnrollmentCaseNotes.tsx +++ b/src/modules/caseNotes/components/EnrollmentCaseNotes.tsx @@ -8,9 +8,9 @@ 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 RelativeDateDisplay from '@/modules/hmis/components/RelativeDateDisplay'; import { getCustomDataElementColumns, - lastUpdatedBy, parseAndFormatDate, } from '@/modules/hmis/hmisUtil'; import { cache } from '@/providers/apolloClient'; @@ -30,10 +30,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 ( + + ); + }, }, }; @@ -112,6 +118,19 @@ const EnrollmentCaseNotes = () => { ]; }, []); + const getTableRowActions = useCallback( + (record: CustomCaseNoteFieldsFragment) => { + return { + primaryAction: { + title: 'View Case Note', + key: 'case note', + onClick: () => onSelectRecord(record), + }, + }; + }, + [onSelectRecord] + ); + const caseNotesFeature = getEnrollmentFeature( DataCollectionFeatureRole.CaseNote ); @@ -145,10 +164,13 @@ const EnrollmentCaseNotes = () => { queryVariables={{ id: enrollmentId }} queryDocument={GetEnrollmentCustomCaseNotesDocument} getColumnDefs={getColumnDefs} + getTableRowActions={getTableRowActions} + getRowAccessibleName={(row) => + parseAndFormatDate(row.informationDate) || row.id + } 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 a4c748f65..8aa386626 100644 --- a/src/modules/clientFiles/components/ClientFilesPage.tsx +++ b/src/modules/clientFiles/components/ClientFilesPage.tsx @@ -1,6 +1,6 @@ import UploadIcon from '@mui/icons-material/Upload'; -import { Box, Chip, Link, Paper, Stack, Typography } from '@mui/material'; -import { useMemo, useState } from 'react'; +import { Box, Chip, Paper, Typography } from '@mui/material'; +import { useCallback, useMemo, useState } from 'react'; import useFileActions from '../hooks/useFileActions'; import FileDialog from './FileModal'; @@ -11,8 +11,7 @@ import { ColumnDef } from '@/components/elements/table/types'; 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 RelativeDateDisplay from '@/modules/hmis/components/RelativeDateDisplay'; import { useClientPermissions, useHasClientPermissions, @@ -75,19 +74,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,33 +100,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}`; }, }, ]; }, [pickListData]); + const getTableRowActions = useCallback( + (file: ClientFileType) => { + return file.redacted + ? {} + : { + primaryAction: { + title: 'View File', + key: 'file', + onClick: () => setViewingFile(file), + }, + }; + }, + [setViewingFile] + ); + return ( <> { queryVariables={{ id: clientId }} queryDocument={GetClientFilesDocument} columns={columns} + getTableRowActions={getTableRowActions} + getRowAccessibleName={(record) => record.name} pagePath='client.files' - handleRowClick={(file) => !file.redacted && setViewingFile(file)} noData='No files' /> diff --git a/src/modules/dataFetching/components/GenericTableWithData.tsx b/src/modules/dataFetching/components/GenericTableWithData.tsx index b62ea70b6..055bb5b61 100644 --- a/src/modules/dataFetching/components/GenericTableWithData.tsx +++ b/src/modules/dataFetching/components/GenericTableWithData.tsx @@ -326,6 +326,7 @@ const GenericTableWithData = < loading={loading && !data} + loadingRegardlessOfData={loading} rows={rows} paginated={!nonTablePagination && !hidePagination} tablePaginationProps={ diff --git a/src/modules/enrollment/components/EnrollmentAssessmentsTable.tsx b/src/modules/enrollment/components/EnrollmentAssessmentsTable.tsx index 20aa6db38..ecb940d5a 100644 --- a/src/modules/enrollment/components/EnrollmentAssessmentsTable.tsx +++ b/src/modules/enrollment/components/EnrollmentAssessmentsTable.tsx @@ -1,12 +1,11 @@ import { useCallback } from 'react'; +import { getViewAssessmentAction } from '@/components/elements/table/tableActions/tableRowActionUtil'; import { ColumnDef } from '@/components/elements/table/types'; -import { - ASSESSMENT_COLUMNS, - generateAssessmentPath, -} from '@/modules/assessments/util'; +import { ASSESSMENT_COLUMNS } from '@/modules/assessments/util'; import GenericTableWithData from '@/modules/dataFetching/components/GenericTableWithData'; import { useFilters } from '@/modules/hmis/filterUtil'; +import { assessmentDescription } from '@/modules/hmis/hmisUtil'; import { AssessmentFieldsFragment, GetEnrollmentAssessmentsDocument, @@ -14,9 +13,9 @@ import { GetEnrollmentAssessmentsQueryVariables, } from '@/types/gqlTypes'; -const columns: ColumnDef[] = [ - ASSESSMENT_COLUMNS.linkedType, +const COLUMNS: ColumnDef[] = [ ASSESSMENT_COLUMNS.date, + ASSESSMENT_COLUMNS.type, ASSESSMENT_COLUMNS.lastUpdated, ]; @@ -31,9 +30,16 @@ const EnrollmentAssessmentsTable: React.FC = ({ enrollmentId, projectId, }) => { - const rowLinkTo = useCallback( - (assessment: AssessmentFieldsFragment) => - generateAssessmentPath(assessment, clientId, enrollmentId), + const getTableRowActions = useCallback( + (assessment: AssessmentFieldsFragment) => { + return { + primaryAction: getViewAssessmentAction( + assessment, + clientId, + enrollmentId + ), + }; + }, [clientId, enrollmentId] ); @@ -51,8 +57,9 @@ const EnrollmentAssessmentsTable: React.FC = ({ filters={filters} queryVariables={{ id: enrollmentId }} queryDocument={GetEnrollmentAssessmentsDocument} - rowLinkTo={rowLinkTo} - columns={columns} + columns={COLUMNS} + getTableRowActions={getTableRowActions} + getRowAccessibleName={(record) => assessmentDescription(record)} pagePath='enrollment.assessments' noData='No assessments' recordType='Assessment' diff --git a/src/modules/enrollment/components/HouseholdAssessmentsTable.tsx b/src/modules/enrollment/components/HouseholdAssessmentsTable.tsx index deeb6f388..7db917374 100644 --- a/src/modules/enrollment/components/HouseholdAssessmentsTable.tsx +++ b/src/modules/enrollment/components/HouseholdAssessmentsTable.tsx @@ -1,13 +1,12 @@ -import { useCallback } from 'react'; - +import { getViewAssessmentAction } from '@/components/elements/table/tableActions/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 } from '@/modules/hmis/hmisUtil'; import { GetHouseholdAssessmentsDocument, GetHouseholdAssessmentsQuery, @@ -18,16 +17,23 @@ 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, ]; +const getTableRowActions = (assessment: HhmAssessmentType) => { + return { + primaryAction: getViewAssessmentAction( + assessment, + assessment.enrollment.client.id, + assessment.enrollment.id + ), + }; +}; + interface Props { householdId: string; projectId: string; @@ -37,16 +43,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 +57,9 @@ const HouseholdAssessmentsTable: React.FC = ({ filters={filters} queryVariables={{ id: householdId }} queryDocument={GetHouseholdAssessmentsDocument} - rowLinkTo={rowLinkTo} - columns={columns} + columns={COLUMNS} + getTableRowActions={getTableRowActions} + getRowAccessibleName={(record) => assessmentDescription(record)} 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..fd65c8f93 100644 --- a/src/modules/enrollment/components/pages/ClientEnrollmentsPage.tsx +++ b/src/modules/enrollment/components/pages/ClientEnrollmentsPage.tsx @@ -2,18 +2,19 @@ import { Paper, Stack, Typography } from '@mui/material'; import { ReactNode, useCallback } from 'react'; import NotCollectedText from '@/components/elements/NotCollectedText'; +import { getViewEnrollmentAction } from '@/components/elements/table/tableActions/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 +24,6 @@ import { ProjectType, RelationshipToHoH, } from '@/types/gqlTypes'; -import { generateSafePath } from '@/utils/pathEncoding'; const CaptionedText: React.FC<{ caption: string; children: ReactNode }> = ({ caption, @@ -39,30 +39,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 +91,41 @@ const columns: ColumnDef[] = [ } }, }, +}; + +const COLUMNS: ColumnDef[] = [ + 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, ]; const ClientEnrollmentsPage = () => { const { client } = useClientDashboardContext(); - const rowLinkTo = useCallback( - (enrollment: ClientEnrollmentFieldsFragment) => { - if (!enrollment.access.canViewEnrollmentDetails) return null; + const filters = useFilters({ + type: 'EnrollmentsForClientFilterOptions', + }); - return generateSafePath(EnrollmentDashboardRoutes.ENROLLMENT_OVERVIEW, { - clientId: client.id, - enrollmentId: enrollment.id, - }); + const getTableRowActions = useCallback( + (enrollment: ClientEnrollmentFieldsFragment) => { + return { + primaryAction: { + ...getViewEnrollmentAction(enrollment, client), + // override the default ariaLabel to provide the project name, since we are in the client context + ariaLabel: `View Enrollment at ${enrollment.projectName} for ${entryExitRange(enrollment)}`, + }, + }; }, [client] ); - const filters = useFilters({ - type: 'EnrollmentsForClientFilterOptions', - }); - return ( <> - - // - // Add Enrollment - // - // - // } - /> + { > queryVariables={{ id: client.id }} queryDocument={GetClientEnrollmentsDocument} - rowLinkTo={rowLinkTo} - columns={columns} + columns={COLUMNS} + getTableRowActions={getTableRowActions} + getRowAccessibleName={(row) => + `${row.projectName} ${entryExitRange(row)}` + } pagePath='client.enrollments' filters={filters} recordType='Enrollment' diff --git a/src/modules/enrollment/components/pages/EnrollmentCeAssessmentsPage.tsx b/src/modules/enrollment/components/pages/EnrollmentCeAssessmentsPage.tsx index d739bf352..6fde42a39 100644 --- a/src/modules/enrollment/components/pages/EnrollmentCeAssessmentsPage.tsx +++ b/src/modules/enrollment/components/pages/EnrollmentCeAssessmentsPage.tsx @@ -22,6 +22,38 @@ import { RecordFormRole, } from '@/types/gqlTypes'; +const COLUMNS: ColumnDef[] = [ + { + header: 'Assessment Date', + render: (a) => parseAndFormatDate(a.assessmentDate), + }, + { + header: 'Assessment Level', + render: (a) => ( + + ), + }, + { + header: 'Assessment Type', + render: (a) => ( + + ), + }, + { + header: 'Assessment Location', + render: (a) => a.assessmentLocation, + }, + { + header: 'Prioritization Status', + render: (a) => ( + + ), + }, +]; + const EnrollmentCeAssessmentsPage = () => { const { enrollment, getEnrollmentFeature } = useEnrollmentDashboardContext(); const enrollmentId = enrollment?.id; @@ -59,52 +91,25 @@ const EnrollmentCeAssessmentsPage = () => { projectId: enrollment?.project.id, }); - const columns: ColumnDef[] = useMemo( - () => [ - { - header: 'Assessment Date', - render: (a) => parseAndFormatDate(a.assessmentDate), - linkTreatment: canEditCeAssessments, - }, - { - header: 'Level', - render: (a) => ( - - ), - }, - { - header: 'Type', - render: (a) => ( - - ), - }, - { - header: 'Location', - render: (a) => a.assessmentLocation, - }, - { - header: 'Prioritization Status', - render: (a) => ( - - ), - }, - ], - [canEditCeAssessments] - ); - const ceAssessmentFeature = getEnrollmentFeature( DataCollectionFeatureRole.CeAssessment ); + const getTableRowActions = useCallback( + (record: CeAssessmentFieldsFragment) => { + return canEditCeAssessments + ? { + primaryAction: { + title: 'View CE Assessment', + key: 'ce assessment', + onClick: () => onSelectRecord(record), + }, + } + : {}; + }, + [onSelectRecord, canEditCeAssessments] + ); + if (!enrollment || !enrollmentId || !clientId || !ceAssessmentFeature) return ; @@ -133,12 +138,14 @@ const EnrollmentCeAssessmentsPage = () => { > queryVariables={{ id: enrollmentId }} queryDocument={GetEnrollmentCeAssessmentsDocument} - columns={columns} + columns={COLUMNS} + getTableRowActions={getTableRowActions} + getRowAccessibleName={(record) => + parseAndFormatDate(record.assessmentDate) || 'unknown date' + } 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 6b9061266..88aac8c85 100644 --- a/src/modules/enrollment/components/pages/EnrollmentCeEventsPage.tsx +++ b/src/modules/enrollment/components/pages/EnrollmentCeEventsPage.tsx @@ -25,18 +25,17 @@ import { RecordFormRole, } from '@/types/gqlTypes'; -const columns: ColumnDef[] = [ - { - header: 'Event Date', - render: (e) => parseAndFormatDate(e.eventDate), - linkTreatment: true, - }, +const COLUMNS: ColumnDef[] = [ { header: 'Event Type', render: (e) => , }, { - header: 'Result', + header: 'Event Date', + render: (e) => parseAndFormatDate(e.eventDate), + }, + { + header: 'Referral Result', render: (e) => eventReferralResult(e), }, ]; @@ -81,6 +80,19 @@ const EnrollmentCeEventsPage = () => { DataCollectionFeatureRole.CeEvent ); + const getTableRowActions = useCallback( + (record: EventFieldsFragment) => { + return { + primaryAction: { + title: 'View CE Event', + key: 'ce event', + onClick: () => onSelectRecord(record), + }, + }; + }, + [onSelectRecord] + ); + if (!enrollment || !enrollmentId || !clientId || !ceEventFeature) return ; @@ -109,11 +121,14 @@ const EnrollmentCeEventsPage = () => { > queryVariables={{ id: enrollmentId }} queryDocument={GetEnrollmentEventsDocument} - columns={columns} + columns={COLUMNS} + getTableRowActions={getTableRowActions} + getRowAccessibleName={(record) => + `${HmisEnums.EventType[record.event]} on ${parseAndFormatDate(record.eventDate)}` + } 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 8041ec2cf..e78a3c8df 100644 --- a/src/modules/enrollment/components/pages/EnrollmentCurrentLivingSituationsPage.tsx +++ b/src/modules/enrollment/components/pages/EnrollmentCurrentLivingSituationsPage.tsx @@ -23,13 +23,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,15 +94,29 @@ 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, ]; }, [] ); + const getTableRowActions = useCallback( + (record: CurrentLivingSituationFieldsFragment) => { + return { + primaryAction: { + title: 'View CLS', + key: 'cls', + ariaLabel: `View Current Living Situation, ${parseAndFormatDate(record.informationDate) || 'unknown date'}`, + onClick: () => onSelectRecord(record), + }, + }; + }, + [onSelectRecord] + ); + if (!enrollment || !enrollmentId || !clientId || !clsFeature) return ; @@ -132,11 +145,14 @@ const EnrollmentCurrentLivingSituationsPage = () => { queryVariables={{ id: enrollmentId }} queryDocument={GetEnrollmentCurrentLivingSituationsDocument} getColumnDefs={getColumnDefs} + getTableRowActions={getTableRowActions} + getRowAccessibleName={(record) => + parseAndFormatDate(record.informationDate) || 'unknown date' + } pagePath='enrollment.currentLivingSituations' noData='No current living situations' recordType='CurrentLivingSituation' headerCellSx={() => ({ color: 'text.secondary' })} - handleRowClick={onSelectRecord} /> {viewRecordDialog()} diff --git a/src/modules/hmis/components/DateWithRelativeTooltip.tsx b/src/modules/hmis/components/DateWithRelativeTooltip.tsx new file mode 100644 index 000000000..48c5260b6 --- /dev/null +++ b/src/modules/hmis/components/DateWithRelativeTooltip.tsx @@ -0,0 +1,59 @@ +import { + Tooltip, + TooltipProps, + Typography, + TypographyProps, +} from '@mui/material'; +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} + + + ); +}; + +export default DateWithRelativeTooltip; diff --git a/src/modules/hmis/components/RelativeDateDisplay.tsx b/src/modules/hmis/components/RelativeDateDisplay.tsx index af82443c2..1ccd01fae 100644 --- a/src/modules/hmis/components/RelativeDateDisplay.tsx +++ b/src/modules/hmis/components/RelativeDateDisplay.tsx @@ -7,15 +7,29 @@ import { 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 +41,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 +56,7 @@ const RelativeDateDisplay = ({ - {formattedDate} + {formattedDate} {tooltipSuffixText} } arrow diff --git a/src/modules/hmis/hmisUtil.ts b/src/modules/hmis/hmisUtil.ts index 6761ff88b..d805b44cb 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, @@ -315,10 +314,7 @@ export const pronouns = (client: ClientFieldsFragment): React.ReactNode => : null; export const entryExitRange = ( - enrollment: - | EnrollmentFieldsFragment - | HouseholdClientFieldsFragment['enrollment'] - | EnrollmentSummaryFieldsFragment, + enrollment: Pick, endPlaceholder?: string ) => { return parseAndFormatDateRange( @@ -376,12 +372,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) => { 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/projectAdministration/components/AllProjectsPage.tsx b/src/modules/projectAdministration/components/AllProjectsPage.tsx index f31813e7b..c1a39b27f 100644 --- a/src/modules/projectAdministration/components/AllProjectsPage.tsx +++ b/src/modules/projectAdministration/components/AllProjectsPage.tsx @@ -1,7 +1,7 @@ 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'; @@ -38,11 +38,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, }, { @@ -61,11 +59,22 @@ const PROJECT_COLUMNS: ColumnDef[] = [ }, ]; +const getProjectTableActions = (project: ProjectAllFieldsFragment) => { + return { + primaryAction: { + title: 'View Project', + key: 'project', + to: generateSafePath(Routes.PROJECT, { + projectId: project.id, + }), + }, + }; +}; + const ORGANIZATION_COLUMNS: ColumnDef[] = [ { header: 'Organization Name', render: 'organizationName', - linkTreatment: true, }, { header: 'Project Count', @@ -87,34 +96,147 @@ 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 getOrganizationTableActions = (organization: OrganizationType) => { + return { + primaryAction: { + title: 'View Organization', + key: 'organization', + to: generateSafePath(Routes.ORGANIZATION, { + organizationId: organization.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} + getTableRowActions={getProjectTableActions} + getRowAccessibleName={(project) => project.projectName} + 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} + getTableRowActions={getOrganizationTableActions} + getRowAccessibleName={(org) => org.organizationName} + 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..d59df9658 100644 --- a/src/modules/projects/components/ProjectAssessments.tsx +++ b/src/modules/projects/components/ProjectAssessments.tsx @@ -1,17 +1,21 @@ import { Paper } from '@mui/material'; -import { useMemo } from 'react'; +import { useCallback } from 'react'; import { useProjectDashboardContext } from './ProjectDashboard'; +import { + getViewAssessmentAction, + getViewEnrollmentAction, +} from '@/components/elements/table/tableActions/tableRowActionUtil'; import { ColumnDef } from '@/components/elements/table/types'; 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, @@ -23,40 +27,38 @@ export type ProjectAssessmentType = NonNullable< GetProjectAssessmentsQuery['project'] >['assessments']['nodes'][number]; +const COLUMNS: ColumnDef[] = [ + ASSESSMENT_CLIENT_NAME_COL, + ASSESSMENT_COLUMNS.date, + ASSESSMENT_COLUMNS.type, + WITH_ENROLLMENT_COLUMNS.entryDate, + WITH_ENROLLMENT_COLUMNS.exitDate, +]; + const ProjectAssessments = () => { const { projectId } = useSafeParams() as { projectId: string; }; const { project } = useProjectDashboardContext(); - const displayColumns: ColumnDef[] = useMemo(() => { - return [ - { - 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); + const getTableRowActions = useCallback( + (record: ProjectAssessmentType) => { + return { + primaryAction: { + ...getViewAssessmentAction( + record, + record.enrollment.client.id, + record.enrollment.id + ), + state: { backToLabel: project.projectName }, + }, + secondaryActions: [ + getViewEnrollmentAction(record.enrollment, record.enrollment.client), + ], + }; + }, + [project] + ); const filters = useFilters({ type: 'AssessmentsForProjectFilterOptions', @@ -74,9 +76,9 @@ const ProjectAssessments = () => { > queryVariables={{ id: projectId }} queryDocument={GetProjectAssessmentsDocument} - rowLinkTo={rowLinkTo} - rowLinkState={{ backToLabel: project.projectName }} - columns={displayColumns} + getTableRowActions={getTableRowActions} + getRowAccessibleName={(record) => assessmentDescription(record)} + 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..85ffff60f 100644 --- a/src/modules/projects/components/ProjectCurrentLivingSituations.tsx +++ b/src/modules/projects/components/ProjectCurrentLivingSituations.tsx @@ -1,12 +1,13 @@ import { Paper } from '@mui/material'; - -import { useCallback, useMemo } from 'react'; +import { getViewEnrollmentAction } from '@/components/elements/table/tableActions/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,50 +17,42 @@ import { } from '@/types/gqlTypes'; import { generateSafePath } from '@/utils/pathEncoding'; -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 COLUMNS: ColumnDef[] = [ + { + header: 'Client Name', + render: (cls: ProjectCurrentLivingSituationFieldsFragment) => ( + + ), + }, + CLS_COLUMNS.informationDate, + CLS_COLUMNS.livingSituation, + WITH_ENROLLMENT_COLUMNS.entryDate, + WITH_ENROLLMENT_COLUMNS.exitDate, +]; - const rowLinkTo = useCallback( - (cls: ProjectCurrentLivingSituationFieldsFragment) => { - return generateSafePath( +const getTableRowActions = ( + cls: ProjectCurrentLivingSituationFieldsFragment +) => { + return { + primaryAction: { + title: 'View CLS', + key: 'cls', + to: generateSafePath( EnrollmentDashboardRoutes.CURRENT_LIVING_SITUATIONS, { clientId: cls.client.id, enrollmentId: cls.enrollment.id, } - ); + ), }, - [] - ); + secondaryActions: [getViewEnrollmentAction(cls.enrollment, cls.client)], + }; +}; + +const ProjectCurrentLivingSituations = () => { + const { projectId } = useSafeParams() as { + projectId: string; + }; return ( <> @@ -72,11 +65,14 @@ const ProjectCurrentLivingSituations = () => { > queryVariables={{ id: projectId }} queryDocument={GetProjectCurrentLivingSituationsDocument} - columns={columns} + columns={COLUMNS} + getTableRowActions={getTableRowActions} + getRowAccessibleName={(record) => + `${clientBriefName(record.client)} ${parseAndFormatDate(record.informationDate) || 'unknown date'}` + } 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 18a32eaca..effe71ec3 100644 --- a/src/modules/projects/components/tables/ProjectClientEnrollmentsTable.tsx +++ b/src/modules/projects/components/tables/ProjectClientEnrollmentsTable.tsx @@ -1,12 +1,17 @@ -import { useCallback, useMemo } from 'react'; +import { useMemo } from 'react'; +import { + getViewClientAction, + getViewEnrollmentAction, +} from '@/components/elements/table/tableActions/tableRowActionUtil'; import { ColumnDef } from '@/components/elements/table/types'; import GenericTableWithData from '@/modules/dataFetching/components/GenericTableWithData'; -import EnrollmentDateRangeWithStatus from '@/modules/hmis/components/EnrollmentDateRangeWithStatus'; +import DateWithRelativeTooltip from '@/modules/hmis/components/DateWithRelativeTooltip'; import EnrollmentStatus from '@/modules/hmis/components/EnrollmentStatus'; import { useFilters } from '@/modules/hmis/filterUtil'; import { clientBriefName, + entryExitRange, formatDateForDisplay, formatDateForGql, parseAndFormatDate, @@ -14,10 +19,6 @@ import { 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 { - ClientDashboardRoutes, - EnrollmentDashboardRoutes, -} from '@/routes/routes'; import { ClientEnrollmentFieldsFragment, EnrollmentFieldsFragment, @@ -30,33 +31,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) => , -}; - -// TODO(#6761) - this is now only used in the client card. Reorganize where column defs live -export const ENROLLMENT_PERIOD_COL: ColumnDef< - | EnrollmentFieldsFragment - | ProjectEnrollmentFieldsFragment - | ClientEnrollmentFieldsFragment -> = { - header: 'Enrollment Period', - render: (e) => ( - - ), -}; - const isHouseholdWithStaff = ( e: any ): e is { household: HouseholdWithStaffAssignmentsFragment } => { @@ -65,20 +44,68 @@ const isHouseholdWithStaff = ( export const ENROLLMENT_COLUMNS: { [key: string]: ColumnDef< - | EnrollmentFieldsFragment - | ProjectEnrollmentFieldsFragment + | ClientEnrollmentFieldsFragment | ProjectEnrollmentQueryEnrollmentFieldsFragment >; } = { - enrollmentStatus: ENROLLMENT_STATUS_COL, entryDate: { header: 'Entry Date', - render: (e) => parseAndFormatDate(e.entryDate), + render: (e) => ( + + ), }, exitDate: { header: 'Exit Date', - render: (e) => parseAndFormatDate(e.exitDate), + render: (e) => { + if (e.exitDate) + return ( + + ); + }, + }, + enrollmentStatus: { + header: 'Status', + render: (e) => , + }, +}; + +export const WITH_ENROLLMENT_COLUMNS: { + [key: string]: ColumnDef; +} = { + entryDate: { + ...ENROLLMENT_COLUMNS.entryDate, + render: (objectWithEnrollment: any) => ( + + ), + }, + exitDate: { + ...ENROLLMENT_COLUMNS.exitDate, + render: (objectWithEnrollment: any) => { + if (objectWithEnrollment.enrollment.exitDate) + return ( + + ); + }, }, +}; + +const COLUMNS: { + [key: string]: ColumnDef< + | EnrollmentFieldsFragment + | ProjectEnrollmentFieldsFragment + | ProjectEnrollmentQueryEnrollmentFieldsFragment + | ClientEnrollmentFieldsFragment + >; +} = { lastClsDate: { header: 'Last Current Living Situation Date', key: 'lastClsDate', @@ -102,6 +129,15 @@ export const ENROLLMENT_COLUMNS: { }, }; +const getTableRowActions = ( + enrollment: ProjectEnrollmentQueryEnrollmentFieldsFragment +) => { + return { + primaryAction: getViewEnrollmentAction(enrollment, enrollment.client), + secondaryActions: [getViewClientAction(enrollment.client)], + }; +}; + const ProjectClientEnrollmentsTable = ({ projectId, columns, @@ -136,7 +172,7 @@ const ProjectClientEnrollmentsTable = ({ ENROLLMENT_COLUMNS.enrollmentStatus, ]; - if (staffAssignmentsEnabled) cols.push(ENROLLMENT_COLUMNS.assignedStaff); + if (staffAssignmentsEnabled) cols.push(COLUMNS.assignedStaff); return cols; }, [staffAssignmentsEnabled]); @@ -151,33 +187,6 @@ const ProjectClientEnrollmentsTable = ({ pickListArgs: { projectId: projectId }, }); - const getTableRowActions = useCallback( - (enrollment: ProjectEnrollmentQueryEnrollmentFieldsFragment) => { - return { - primaryAction: { - title: 'View Enrollment', - key: 'enrollment', - ariaLabel: `View Enrollment, ${clientBriefName(enrollment.client)}`, - to: generateSafePath(EnrollmentDashboardRoutes.ENROLLMENT_OVERVIEW, { - clientId: enrollment.client.id, - enrollmentId: enrollment.id, - }), - }, - secondaryActions: [ - { - title: 'View Client', - key: 'client', - ariaLabel: `View Client, ${clientBriefName(enrollment.client)}`, - to: generateSafePath(ClientDashboardRoutes.PROFILE, { - clientId: enrollment.client.id, - }), - }, - ], - }; - }, - [] - ); - return ( { const result: Partial = {}; - if (cols.includes(ENROLLMENT_COLUMNS.lastClsDate.key || '')) + if (cols.includes(COLUMNS.lastClsDate.key || '')) result.includeCls = true; - if (cols.includes(ENROLLMENT_COLUMNS.assignedStaff.key || '')) + if (cols.includes(COLUMNS.assignedStaff.key || '')) result.includeStaffAssignment = true; return result; @@ -218,7 +227,9 @@ const ProjectClientEnrollmentsTable = ({ getTableRowActions={getTableRowActions} getRowAccessibleName={( enrollment: ProjectEnrollmentQueryEnrollmentFieldsFragment - ) => clientBriefName(enrollment.client)} + ) => + `${clientBriefName(enrollment.client)} ${entryExitRange(enrollment)}` + } /> ); }; diff --git a/src/modules/projects/components/tables/ProjectExternalSubmissionsTable.tsx b/src/modules/projects/components/tables/ProjectExternalSubmissionsTable.tsx index e0ca3664a..2dd6e7b3f 100644 --- a/src/modules/projects/components/tables/ProjectExternalSubmissionsTable.tsx +++ b/src/modules/projects/components/tables/ProjectExternalSubmissionsTable.tsx @@ -1,4 +1,4 @@ -import { Button, Chip, Stack, Typography } from '@mui/material'; +import { Chip, Stack, Typography } from '@mui/material'; import { capitalize } from 'lodash-es'; import { useCallback, useState } from 'react'; import LoadingButton from '@/components/elements/LoadingButton'; @@ -101,20 +101,22 @@ const ProjectExternalSubmissionsTable = ({ ), }, ...defs, - { - header: 'Action', - render: ({ id }: ExternalFormSubmissionSummaryFragment) => ( - - ), - }, ]; }, + [] + ); + + const getTableRowActions = useCallback( + ({ id }: ExternalFormSubmissionSummaryFragment) => { + return { + primaryAction: { + title: 'View Submission', + key: 'submission', + onClick: () => setModalOpenId(id), + disabled: bulkLoading, + }, + }; + }, [setModalOpenId, bulkLoading] ); @@ -140,6 +142,7 @@ const ProjectExternalSubmissionsTable = ({ } queryDocument={GetProjectExternalFormSubmissionsDocument} getColumnDefs={getColumnDefs} + getTableRowActions={getTableRowActions} noData='No external form submissions' pagePath='project.externalFormSubmissions' recordType='ExternalFormSubmission' diff --git a/src/modules/projects/components/tables/ProjectHouseholdsTable.tsx b/src/modules/projects/components/tables/ProjectHouseholdsTable.tsx index da04e3186..84e07d5bc 100644 --- a/src/modules/projects/components/tables/ProjectHouseholdsTable.tsx +++ b/src/modules/projects/components/tables/ProjectHouseholdsTable.tsx @@ -10,9 +10,12 @@ import { } from '@mui/material'; import { visuallyHidden } from '@mui/utils'; import React, { ReactNode, useMemo } from 'react'; +import { + getViewClientAction, + getViewEnrollmentAction, +} from '@/components/elements/table/tableActions/tableRowActionUtil'; import TableRowActions from '@/components/elements/table/TableRowActions'; import { ColumnDef } from '@/components/elements/table/types'; -import ClientName from '@/modules/client/components/ClientName'; import GenericTableWithData from '@/modules/dataFetching/components/GenericTableWithData'; import EnrollmentClientNameWithAge from '@/modules/hmis/components/EnrollmentClientNameWithAge'; import EnrollmentDateRangeWithStatus from '@/modules/hmis/components/EnrollmentDateRangeWithStatus'; @@ -25,13 +28,13 @@ import { formatDateForDisplay, formatDateForGql, hohSort, - parseAndFormatDate, } from '@/modules/hmis/hmisUtil'; import { useProjectDashboardContext } from '@/modules/projects/components/ProjectDashboard'; import { - ClientDashboardRoutes, - EnrollmentDashboardRoutes, -} from '@/routes/routes'; + ENROLLMENT_COLUMNS, + WITH_ENROLLMENT_COLUMNS, +} from '@/modules/projects/components/tables/ProjectClientEnrollmentsTable'; +import { CLIENT_COLUMNS } from '@/modules/search/components/ClientSearch'; import { HmisEnums } from '@/types/gqlEnums'; import { GetProjectHouseholdsDocument, @@ -43,7 +46,6 @@ import { ProjectEnrollmentsHouseholdFieldsFragment, RelationshipToHoH, } from '@/types/gqlTypes'; -import { generateSafePath } from '@/utils/pathEncoding'; export type HouseholdFields = NonNullable< GetProjectHouseholdsQuery['project'] @@ -63,13 +65,15 @@ export const ASSIGNED_STAFF_COL = { 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); + const allNames = hh.staffAssignments.nodes.map( + (staffAssignment) => staffAssignment.user.name + ); + + const first = allNames[0]; + const rest = allNames.slice(1); return ( - <> + {first}{' '} {rest.length > 0 && ( @@ -80,7 +84,7 @@ export const ASSIGNED_STAFF_COL = { /> )} - + ); }, }; @@ -184,25 +188,8 @@ const getTableRowActions = ( record: ProjectEnrollmentsHouseholdClientFieldsFragment ) => { return { - primaryAction: { - title: 'View Enrollment', - key: 'enrollment', - ariaLabel: `View Enrollment, ${clientBriefName(record.client)}`, - to: generateSafePath(EnrollmentDashboardRoutes.ENROLLMENT_OVERVIEW, { - clientId: record.client.id, - enrollmentId: record.enrollment.id, - }), - }, - secondaryActions: [ - { - title: 'View Client', - key: 'client', - ariaLabel: `View Client, ${clientBriefName(record.client)}`, - to: generateSafePath(ClientDashboardRoutes.PROFILE, { - clientId: record.client.id, - }), - }, - ], + primaryAction: getViewEnrollmentAction(record.enrollment, record.client), + secondaryActions: [getViewClientAction(record.client)], }; }; @@ -212,7 +199,7 @@ interface ProjectHouseholdsClientRowProps { lastInGroup?: boolean; showAssignedStaff?: boolean; } -// TODO(#6761) it could be nice to keep these in render functions and refer to those + const ProjectHouseholdsClientRow: React.FC = ({ household, householdClient, @@ -227,9 +214,15 @@ const ProjectHouseholdsClientRow: React.FC = ({ return ( - + {(CLIENT_COLUMNS.name.render as CallableFunction)( + householdClient.client + )} + + + {(CLIENT_COLUMNS.age.render as CallableFunction)( + householdClient.client + )} - {householdClient.client.age} = ({ /> - {parseAndFormatDate(householdClient.enrollment.entryDate)} + {(WITH_ENROLLMENT_COLUMNS.entryDate.render as CallableFunction)( + householdClient + )} - {parseAndFormatDate(householdClient.enrollment.exitDate)} + {(WITH_ENROLLMENT_COLUMNS.exitDate.render as CallableFunction)( + householdClient + )} - + {(ENROLLMENT_COLUMNS.enrollmentStatus.render as CallableFunction)( + householdClient.enrollment + )} {showAssignedStaff && ( diff --git a/src/modules/search/components/ClientSearch.tsx b/src/modules/search/components/ClientSearch.tsx index f0872845c..7de3737bd 100644 --- a/src/modules/search/components/ClientSearch.tsx +++ b/src/modules/search/components/ClientSearch.tsx @@ -14,9 +14,8 @@ 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 { getViewClientAction } from '@/components/elements/table/tableActions/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 +29,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, @@ -48,7 +47,6 @@ import { SearchClientsQuery, SearchClientsQueryVariables, } from '@/types/gqlTypes'; -import { generateSafePath } from '@/utils/pathEncoding'; function asClient( record: @@ -67,20 +65,6 @@ export const CLIENT_COLUMNS: { | ProjectEnrollmentFieldsFragment >; } = { - id: { header: 'HMIS ID', render: 'id' }, - linkedId: { - header: 'ID', - render: (client) => ( - - {client.id} - - ), - }, name: { header: 'Name', key: 'name', @@ -91,11 +75,6 @@ export const CLIENT_COLUMNS: { key: 'age', render: (client) => asClient(client).age, }, - linkedName: { - header: 'Name', - key: 'name', - render: (client) => , - }, linkedNameNewTab: { header: 'Name', key: 'name', @@ -135,27 +114,15 @@ export const CLIENT_COLUMNS: { }, }; -export const SEARCH_RESULT_COLUMNS: ColumnDef[] = [ - CLIENT_COLUMNS.id, - { - ...CLIENT_COLUMNS.first, - linkTreatment: true, - ariaLabel: (row) => clientNameAllParts(row), - }, - { ...CLIENT_COLUMNS.last, linkTreatment: true }, - { ...CLIENT_COLUMNS.ssn, width: '150px' }, - { ...CLIENT_COLUMNS.dobAge, width: '180px' }, -]; +const getSearchResultTableActions = (record: ClientFieldsFragment) => { + return { + primaryAction: getViewClientAction(record), + }; +}; -export const MOBILE_SEARCH_RESULT_COLUMNS: ColumnDef[] = [ - CLIENT_COLUMNS.id, - { - ...CLIENT_COLUMNS.name, - linkTreatment: true, - ariaLabel: (row) => clientNameAllParts(row), - }, - { ...CLIENT_COLUMNS.ssn }, - { ...CLIENT_COLUMNS.dobAge }, +const SEARCH_RESULT_COLUMNS: ColumnDef[] = [ + CLIENT_COLUMNS.name, + CLIENT_COLUMNS.age, ]; /** @@ -176,8 +143,6 @@ const ClientSearch = () => { // whether search has occurred const [hasSearched, setHasSearched] = useState(false); - const isMobile = useIsMobile(); - const [searchInput, setSearchInput] = useState( null ); @@ -195,22 +160,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 @@ -231,14 +189,6 @@ const ClientSearch = () => { } }, [derivedSearchParams, searchParams]); - const rowLinkTo = useCallback( - (row: ClientFieldsFragment) => - generateSafePath(ClientDashboardRoutes.PROFILE, { - clientId: row.id, - }), - [] - ); - const onClearSearch = useCallback(() => { setSearchInput(null); setSearchParams({}); @@ -324,8 +274,9 @@ const ClientSearch = () => { queryVariables={{ input: searchInput }} queryDocument={SearchClientsDocument} onCompleted={() => setHasSearched(true)} - rowLinkTo={rowLinkTo} columns={columns} + getTableRowActions={getSearchResultTableActions} + getRowAccessibleName={(record) => clientBriefName(record)} pagePath='clientSearch' fetchPolicy='cache-and-network' filters={filters} diff --git a/src/modules/services/components/ClientServicesPage.tsx b/src/modules/services/components/ClientServicesPage.tsx index d814a1f1d..5f42df1a8 100644 --- a/src/modules/services/components/ClientServicesPage.tsx +++ b/src/modules/services/components/ClientServicesPage.tsx @@ -1,26 +1,28 @@ import { Paper } from '@mui/material'; -import { useMemo } from 'react'; +import { useCallback, useMemo } from 'react'; -import RouterLink from '@/components/elements/RouterLink'; +import { + getViewEnrollmentAction, + getViewServiceAction, +} from '@/components/elements/table/tableActions/tableRowActionUtil'; import { ColumnDef } from '@/components/elements/table/types'; 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 { 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'] @@ -36,35 +38,12 @@ const ClientServicesPage: React.FC<{ () => ( [ - 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, @@ -77,13 +56,33 @@ const ClientServicesPage: React.FC<{ return true; }), - [clientId, omitColumns] + [omitColumns] ); const filters = useFilters({ type: 'ServiceFilterOptions', }); + const getTableRowActions = useCallback( + (service: ServiceType) => { + return { + primaryAction: getViewServiceAction( + service, + service.enrollment.id, + clientId + ), + secondaryActions: [ + { + ...getViewEnrollmentAction(service.enrollment, { id: clientId }), + // override the default ariaLabel to provide the project name, since we are in the client context + ariaLabel: `View Enrollment at ${service.enrollment.projectName} for ${entryExitRange(service.enrollment)}`, + }, + ], + }; + }, + [clientId] + ); + return ( <> @@ -97,6 +96,10 @@ const ClientServicesPage: React.FC<{ queryVariables={{ id: clientId }} queryDocument={GetClientServicesDocument} columns={columns} + getTableRowActions={getTableRowActions} + getRowAccessibleName={(record) => + `${getServiceTypeForDisplay(record.serviceType)} on ${parseAndFormatDate(record.dateProvided)}` + } pagePath='client.services' fetchPolicy='cache-and-network' noData='No services' diff --git a/src/modules/services/components/EnrollmentServicesPage.tsx b/src/modules/services/components/EnrollmentServicesPage.tsx index 3251c99ba..4ce3ad2ba 100644 --- a/src/modules/services/components/EnrollmentServicesPage.tsx +++ b/src/modules/services/components/EnrollmentServicesPage.tsx @@ -1,14 +1,16 @@ import AddIcon from '@mui/icons-material/Add'; import { Button } from '@mui/material'; -import { useState } from 'react'; +import { useCallback, useState } from 'react'; 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'; @@ -20,6 +22,12 @@ import { ServiceFieldsFragment, } from '@/types/gqlTypes'; +const COLUMNS = [ + SERVICE_BASIC_COLUMNS.serviceDate, + SERVICE_BASIC_COLUMNS.serviceType, + SERVICE_COLUMNS.serviceDetails, +]; + const EnrollmentServicesPage = () => { const { enrollment, getEnrollmentFeature } = useEnrollmentDashboardContext(); const enrollmentId = enrollment?.id; @@ -43,16 +51,28 @@ const EnrollmentServicesPage = () => { DataCollectionFeatureRole.Service ); - if (!enrollment || !enrollmentId || !clientId || !serviceFeature) - return ; + const canEditServices = enrollment?.access.canEditEnrollments; - const canEditServices = enrollment.access.canEditEnrollments; + const getTableRowActions = useCallback( + (service: ServiceFieldsFragment) => { + return canEditServices + ? { + primaryAction: { + title: 'Update Service', + key: 'service', + onClick: () => { + 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 ( <> @@ -77,17 +97,13 @@ const EnrollmentServicesPage = () => { GetEnrollmentServicesQueryVariables, ServiceFieldsFragment > - handleRowClick={ - canEditServices - ? (record) => { - setViewingRecord(record); - openServiceDialog(); - } - : undefined - } queryVariables={{ id: enrollmentId }} queryDocument={GetEnrollmentServicesDocument} - columns={columns} + columns={COLUMNS} + getTableRowActions={getTableRowActions} + getRowAccessibleName={(record) => + `${getServiceTypeForDisplay(record.serviceType)} on ${parseAndFormatDate(record.dateProvided)}` + } pagePath='enrollment.services' noData='No services' recordType='Service' diff --git a/src/modules/services/components/ProjectServicesTable.tsx b/src/modules/services/components/ProjectServicesTable.tsx index b7830673a..e85e4356b 100644 --- a/src/modules/services/components/ProjectServicesTable.tsx +++ b/src/modules/services/components/ProjectServicesTable.tsx @@ -1,25 +1,44 @@ -import { useCallback, useMemo } from 'react'; +import { useMemo } from 'react'; +import { + getViewEnrollmentAction, + getViewServiceAction, +} from '@/components/elements/table/tableActions/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'] >['services']['nodes'][number]; +const getTableRowActions = (service: ServiceFields) => { + return { + primaryAction: getViewServiceAction( + service, + service.enrollment.id, + service.enrollment.client.id + ), + secondaryActions: [ + getViewEnrollmentAction(service.enrollment, service.enrollment.client), + ], + }; +}; + const ProjectServicesTable = ({ projectId, columns, @@ -31,27 +50,15 @@ const ProjectServicesTable = ({ if (columns) return columns; return [ { - header: 'First Name', - linkTreatment: true, + header: 'Client Name', render: (s: ServiceFields) => ( - + ), }, - { - header: 'Last Name', - linkTreatment: true, - render: (s: ServiceFields) => ( - - ), - }, - { ...SERVICE_BASIC_COLUMNS.dateProvided, linkTreatment: false }, + { ...SERVICE_BASIC_COLUMNS.serviceDate, linkTreatment: false }, SERVICE_BASIC_COLUMNS.serviceType, - { - header: 'Enrollment Period', - render: (s: ServiceFields) => ( - - ), - }, + WITH_ENROLLMENT_COLUMNS.entryDate, + WITH_ENROLLMENT_COLUMNS.exitDate, ]; }, [columns]); @@ -59,13 +66,6 @@ const ProjectServicesTable = ({ type: 'ServicesForProjectFilterOptions', }); - const rowLinkTo = useCallback((s: ServiceFields) => { - return generateSafePath(EnrollmentDashboardRoutes.SERVICES, { - clientId: s.enrollment.client.id, - enrollmentId: s.enrollment.id, - }); - }, []); - return ( + `${clientBriefName(record.enrollment.client)}'s ${getServiceTypeForDisplay(record.serviceType)} on ${parseAndFormatDate(record.dateProvided)}` + } noData='No services' pagePath='project.services' recordType='Service' filters={filters} - rowLinkTo={rowLinkTo} /> ); }; 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), }, }; From 50b472b17ce5b3ed8c1797670764e2c9a87f0008 Mon Sep 17 00:00:00 2001 From: martha Date: Mon, 16 Dec 2024 14:10:52 -0500 Subject: [PATCH 02/13] Add random comment --- .../enrollment/components/EnrollmentAssessmentActionButtons.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/modules/enrollment/components/EnrollmentAssessmentActionButtons.tsx b/src/modules/enrollment/components/EnrollmentAssessmentActionButtons.tsx index bc8077993..8f48b782b 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 ( Date: Wed, 18 Dec 2024 16:06:46 -0500 Subject: [PATCH 03/13] Add storybook for table actions and fix cde story --- .../elements/table/GenericTable.stories.tsx | 29 +++++++++++++++++-- 1 file changed, 27 insertions(+), 2 deletions(-) diff --git a/src/components/elements/table/GenericTable.stories.tsx b/src/components/elements/table/GenericTable.stories.tsx index c7116e42c..eff2773f0 100644 --- a/src/components/elements/table/GenericTable.stories.tsx +++ b/src/components/elements/table/GenericTable.stories.tsx @@ -3,7 +3,10 @@ import { Meta, StoryFn } from '@storybook/react'; import GenericTable, { Props as GenericTableProps } from './GenericTable'; 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'; @@ -135,8 +138,30 @@ 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], + getTableRowActions: (record) => { + return { + primaryAction: { + title: 'Do something in-app', + key: 'onClick', + onClick: () => alert(`Hello, ${clientBriefName(record)} ${record.id}`), + }, + secondaryActions: [ + { + title: 'Navigate to a link', + key: 'link', + to: 'https://storybook.js.org/docs', // just link somewhere random to show `to` prop working + }, + ], + }; + }, + getRowAccessibleName: (record) => `${clientBriefName(record)} ${record.id}`, +}; From 5c4d33aeb1b63b569e4ed9d9cbed32806daa4a02 Mon Sep 17 00:00:00 2001 From: martha Date: Mon, 30 Dec 2024 20:03:59 -0500 Subject: [PATCH 04/13] Implement PR suggestion of moving table row to column def --- .../elements/table/GenericTable.tsx | 28 +------ .../elements/table/TableRowActions.tsx | 34 ++++---- .../tables/ProjectClientEnrollmentsTable.tsx | 78 ++++++++++--------- .../tables/ProjectHouseholdsTable.tsx | 49 ++++++------ 4 files changed, 79 insertions(+), 110 deletions(-) diff --git a/src/components/elements/table/GenericTable.tsx b/src/components/elements/table/GenericTable.tsx index 47472b350..2a546af9d 100644 --- a/src/components/elements/table/GenericTable.tsx +++ b/src/components/elements/table/GenericTable.tsx @@ -17,7 +17,6 @@ import { TableRow, Theme, } from '@mui/material'; -import { visuallyHidden } from '@mui/utils'; import { compact, get, includes, isNil, without } from 'lodash-es'; import { ComponentType, @@ -41,9 +40,6 @@ import { isRenderFunction, RenderFunction, } from './types'; -import TableRowActions, { - TableRowActionsType, -} from '@/components/elements/table/TableRowActions'; import { LocationState } from '@/routes/routeUtil'; export interface Props { @@ -78,7 +74,6 @@ export interface Props { // 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; injectBelowRows?: ReactNode; // component to inject below all rendered rows, above footer - getTableRowActions?: (record: T) => TableRowActionsType; getRowAccessibleName?: (row: T) => string; } @@ -138,8 +133,6 @@ const GenericTable = ({ rowLinkState, TableBodyComponent = TableBody, injectBelowRows, - getTableRowActions, - getRowAccessibleName, }: Props) => { const columns = useMemo( () => (columnProp || []).filter((c) => !c.hide), @@ -206,8 +199,7 @@ const GenericTable = ({ const key = (def: ColumnDef) => def.key || (typeof def.header === 'string' ? def.header : ''); - const fullColSpan = - columns.length + (selectable ? 1 : 0) + (getTableRowActions ? 1 : 0); + const fullColSpan = columns.length + (selectable ? 1 : 0); const tableHead = noHead ? null : vertical ? ( {renderVerticalHeaderCell && ( @@ -255,11 +247,6 @@ const GenericTable = ({ {def.header} ))} - {getTableRowActions && ( - - Actions - - )} )} {loading && loadingVariant === 'linear' && ( @@ -464,19 +451,6 @@ const GenericTable = ({ ); })} - {getTableRowActions && ( - - - - )} ); })} diff --git a/src/components/elements/table/TableRowActions.tsx b/src/components/elements/table/TableRowActions.tsx index 6dd073223..a6fff8866 100644 --- a/src/components/elements/table/TableRowActions.tsx +++ b/src/components/elements/table/TableRowActions.tsx @@ -1,51 +1,47 @@ import { Stack } from '@mui/material'; -import { useMemo } from 'react'; +import { ReactNode, useMemo } from 'react'; import ButtonLink from '../ButtonLink'; import CommonMenuButton, { CommonMenuItem } from '../CommonMenuButton'; -export type TableRowActionsType = { - primaryAction?: CommonMenuItem; - secondaryActions?: CommonMenuItem[]; -}; - interface TableRowActionsProps { record: T; recordName?: string; - getActions: (record: T) => TableRowActionsType; + // todo @martha - add some commentary here + primaryActionConfig?: CommonMenuItem; + primaryAction?: ReactNode; + secondaryActionConfigs?: CommonMenuItem[]; } const TableRowActions = ({ record, recordName, - getActions, + primaryAction, + primaryActionConfig, + secondaryActionConfigs, }: TableRowActionsProps) => { const accessibleName = useMemo( () => recordName || record.id, [record.id, recordName] ); - const { primaryAction, secondaryActions } = useMemo( - () => getActions(record), - [getActions, record] - ); - return ( - {!!primaryAction && ( + {!!primaryActionConfig && ( - {primaryAction.title} + {primaryActionConfig.title} )} - {!!secondaryActions && secondaryActions.length > 0 && ( + {primaryAction} + {!!secondaryActionConfigs && secondaryActionConfigs.length > 0 && ( [] = useMemo(() => { - const cols = [ + return [ CLIENT_COLUMNS.name, CLIENT_COLUMNS.age, ENROLLMENT_COLUMNS.entryDate, ENROLLMENT_COLUMNS.exitDate, ENROLLMENT_COLUMNS.enrollmentStatus, + ...(staffAssignmentsEnabled ? [ENROLLMENT_COLUMNS.assignedStaff] : []), + { + header: Actions, + key: 'Actions', + tableCellProps: { sx: { p: 0 } }, + render: (row: ProjectEnrollmentQueryEnrollmentFieldsFragment) => { + return ( + + ); + }, + }, ]; - - if (staffAssignmentsEnabled) cols.push(ENROLLMENT_COLUMNS.assignedStaff); - - return cols; }, [staffAssignmentsEnabled]); const filters = useFilters({ @@ -151,33 +186,6 @@ const ProjectClientEnrollmentsTable = ({ pickListArgs: { projectId: projectId }, }); - const getTableRowActions = useCallback( - (enrollment: ProjectEnrollmentQueryEnrollmentFieldsFragment) => { - return { - primaryAction: { - title: 'View Enrollment', - key: 'enrollment', - ariaLabel: `View Enrollment, ${clientBriefName(enrollment.client)}`, - to: generateSafePath(EnrollmentDashboardRoutes.ENROLLMENT_OVERVIEW, { - clientId: enrollment.client.id, - enrollmentId: enrollment.id, - }), - }, - secondaryActions: [ - { - title: 'View Client', - key: 'client', - ariaLabel: `View Client, ${clientBriefName(enrollment.client)}`, - to: generateSafePath(ClientDashboardRoutes.PROFILE, { - clientId: enrollment.client.id, - }), - }, - ], - }; - }, - [] - ); - return ( clientBriefName(enrollment.client)} /> ); }; diff --git a/src/modules/projects/components/tables/ProjectHouseholdsTable.tsx b/src/modules/projects/components/tables/ProjectHouseholdsTable.tsx index 8d449d184..c241c3718 100644 --- a/src/modules/projects/components/tables/ProjectHouseholdsTable.tsx +++ b/src/modules/projects/components/tables/ProjectHouseholdsTable.tsx @@ -73,32 +73,6 @@ export const ASSIGNED_STAFF_COL = { }, }; -const getTableRowActions = ( - record: ProjectEnrollmentsHouseholdClientFieldsFragment -) => { - return { - primaryAction: { - title: 'View Enrollment', - key: 'enrollment', - ariaLabel: `View Enrollment, ${clientBriefName(record.client)}`, - to: generateSafePath(EnrollmentDashboardRoutes.ENROLLMENT_OVERVIEW, { - clientId: record.client.id, - enrollmentId: record.enrollment.id, - }), - }, - secondaryActions: [ - { - title: 'View Client', - key: 'client', - ariaLabel: `View Client, ${clientBriefName(record.client)}`, - to: generateSafePath(ClientDashboardRoutes.PROFILE, { - clientId: record.client.id, - }), - }, - ], - }; -}; - interface ProjectHouseholdsClientRowProps { household: ProjectEnrollmentsHouseholdFieldsFragment; householdClient: ProjectEnrollmentsHouseholdClientFieldsFragment; @@ -152,7 +126,28 @@ const ProjectHouseholdsClientRow: React.FC = ({ From 2feec78b6423070467f735aa2ad979d1dae18607 Mon Sep 17 00:00:00 2001 From: martha Date: Tue, 31 Dec 2024 15:46:33 -0500 Subject: [PATCH 05/13] add todo --- src/components/elements/table/GenericTable.stories.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/elements/table/GenericTable.stories.tsx b/src/components/elements/table/GenericTable.stories.tsx index eff2773f0..13e21386e 100644 --- a/src/components/elements/table/GenericTable.stories.tsx +++ b/src/components/elements/table/GenericTable.stories.tsx @@ -143,6 +143,7 @@ WithCustomDataElements.args = { ], }; +// todo @martha - fix this export const WithTableRowActions = Template().bind({}); WithTableRowActions.args = { rows: fakeRows, From 0605c2ab9034998c6328f2e585f720545bb003a8 Mon Sep 17 00:00:00 2001 From: martha Date: Thu, 2 Jan 2025 12:27:28 -0500 Subject: [PATCH 06/13] Finish refactor to use column def instead of getTableAction --- .../elements/table/GenericTable.stories.tsx | 46 +++++--- .../elements/table/TableRowActions.tsx | 23 +++- .../elements/table/tableRowActionUtil.tsx | 2 +- .../components/services/ServiceTypeTable.tsx | 32 ++--- .../pages/ClientAssessmentsPage.tsx | 54 +++++---- .../components/BulkServicesTable.tsx | 70 ++++++----- .../caseNotes/components/ClientCaseNotes.tsx | 57 ++++----- .../components/EnrollmentCaseNotes.tsx | 48 ++++---- .../components/ClientFilesPage.tsx | 39 +++--- .../components/GenericTableWithData.tsx | 2 +- .../components/EnrollmentAssessmentsTable.tsx | 47 ++++---- .../components/HouseholdAssessmentsTable.tsx | 41 ++++--- .../pages/ClientEnrollmentsPage.tsx | 58 ++++----- .../pages/EnrollmentCeAssessmentsPage.tsx | 111 ++++++++++-------- .../pages/EnrollmentCeEventsPage.tsx | 65 +++++----- .../EnrollmentCurrentLivingSituationsPage.tsx | 37 +++--- .../hmis/components/EnrollmentStatus.tsx | 9 +- .../components/AllProjectsPage.tsx | 62 +++++----- .../components/ProjectAssessments.tsx | 69 ++++++----- .../ProjectCurrentLivingSituations.tsx | 52 ++++---- .../tables/ProjectClientEnrollmentsTable.tsx | 5 +- .../ProjectExternalSubmissionsTable.tsx | 32 ++--- .../search/components/ClientSearch.tsx | 24 ++-- .../components/ClientServicesPage.tsx | 55 ++++----- .../components/EnrollmentServicesPage.tsx | 59 +++++----- .../components/ProjectServicesTable.tsx | 41 ++++--- 26 files changed, 634 insertions(+), 506 deletions(-) diff --git a/src/components/elements/table/GenericTable.stories.tsx b/src/components/elements/table/GenericTable.stories.tsx index 13e21386e..3af453f32 100644 --- a/src/components/elements/table/GenericTable.stories.tsx +++ b/src/components/elements/table/GenericTable.stories.tsx @@ -2,6 +2,8 @@ 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 { clientBriefName, @@ -143,26 +145,32 @@ WithCustomDataElements.args = { ], }; -// todo @martha - fix this export const WithTableRowActions = Template().bind({}); WithTableRowActions.args = { rows: fakeRows, - columns: [CLIENT_COLUMNS.name], - getTableRowActions: (record) => { - return { - primaryAction: { - title: 'Do something in-app', - key: 'onClick', - onClick: () => alert(`Hello, ${clientBriefName(record)} ${record.id}`), - }, - secondaryActions: [ - { - title: 'Navigate to a link', - key: 'link', - to: 'https://storybook.js.org/docs', // just link somewhere random to show `to` prop working - }, - ], - }; - }, - getRowAccessibleName: (record) => `${clientBriefName(record)} ${record.id}`, + 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/TableRowActions.tsx b/src/components/elements/table/TableRowActions.tsx index 2e74334ec..059dc7624 100644 --- a/src/components/elements/table/TableRowActions.tsx +++ b/src/components/elements/table/TableRowActions.tsx @@ -1,4 +1,4 @@ -import { Stack } from '@mui/material'; +import { Button, Stack } from '@mui/material'; import { ReactNode } from 'react'; import ButtonLink from '../ButtonLink'; import CommonMenuButton, { CommonMenuItem } from '../CommonMenuButton'; @@ -22,19 +22,36 @@ const TableRowActions = ({ secondaryActionConfigs, }: TableRowActionsProps) => { // todo @martha - confirm that secondary actions have correct aria, maybe require it on the type? + // proposal is to support alternative way of providing aria for primary, but secondaries its ok return ( - {!!primaryActionConfig && ( + {!!primaryActionConfig && primaryActionConfig.to && ( {primaryActionConfig.title} )} + {!!primaryActionConfig && primaryActionConfig.onClick && ( + + )} {primaryAction} {!!secondaryActionConfigs && secondaryActionConfigs.length > 0 && ( = { header: Actions, key: 'Actions', - tableCellProps: { sx: { p: 0 } }, + tableCellProps: { sx: { py: 0 } }, render: '', // gets overridden when used }; diff --git a/src/modules/admin/components/services/ServiceTypeTable.tsx b/src/modules/admin/components/services/ServiceTypeTable.tsx index 5b6c3f081..cc7417968 100644 --- a/src/modules/admin/components/services/ServiceTypeTable.tsx +++ b/src/modules/admin/components/services/ServiceTypeTable.tsx @@ -1,4 +1,6 @@ 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'; @@ -40,20 +42,24 @@ const COLUMNS: ColumnDef[] = [ ) : null, }, + { + ...BASE_ACTION_COLUMN_DEF, + render: (row: ServiceTypeConfigFieldsFragment) => ( + + ), + }, ]; -const getTableRowActions = (row: ServiceTypeConfigFieldsFragment) => { - return { - primaryAction: { - title: 'View Service Type', - key: 'service type', - to: generateSafePath(AdminDashboardRoutes.CONFIGURE_SERVICE_TYPE, { - serviceTypeId: row.id, - }), - }, - }; -}; - const ServiceTypeTable = () => { const filters = useFilters({ type: 'ServiceTypeFilterOptions', @@ -70,8 +76,6 @@ const ServiceTypeTable = () => { queryVariables={{}} queryDocument={GetServiceTypesDocument} columns={COLUMNS} - getTableRowActions={getTableRowActions} - getRowAccessibleName={(record) => getServiceTypeForDisplay(record)} pagePath='serviceTypes' noData='No service types' filters={filters} diff --git a/src/modules/assessments/components/pages/ClientAssessmentsPage.tsx b/src/modules/assessments/components/pages/ClientAssessmentsPage.tsx index 880de8e97..7b873128c 100644 --- a/src/modules/assessments/components/pages/ClientAssessmentsPage.tsx +++ b/src/modules/assessments/components/pages/ClientAssessmentsPage.tsx @@ -1,7 +1,11 @@ import { Paper } from '@mui/material'; -import { useCallback } from 'react'; +import { useMemo } from 'react'; -import { getViewAssessmentAction } from '@/components/elements/table/tableActions/tableRowActionUtil'; +import TableRowActions from '@/components/elements/table/TableRowActions'; +import { + BASE_ACTION_COLUMN_DEF, + getViewAssessmentAction, +} from '@/components/elements/table/tableRowActionUtil'; import { ColumnDef } from '@/components/elements/table/types'; import PageTitle from '@/components/layout/PageTitle'; import useSafeParams from '@/hooks/useSafeParams'; @@ -17,16 +21,6 @@ import { GetClientAssessmentsQueryVariables, } from '@/types/gqlTypes'; -const COLUMNS: ColumnDef[] = [ - ASSESSMENT_COLUMNS.date, - { - header: 'Project Name', - render: (row) => row.enrollment.projectName, - }, - ASSESSMENT_COLUMNS.type, - ASSESSMENT_COLUMNS.lastUpdated, -]; - const ClientAssessmentsPage = () => { const { clientId } = useSafeParams() as { clientId: string }; @@ -34,16 +28,30 @@ const ClientAssessmentsPage = () => { type: 'AssessmentFilterOptions', }); - const getTableRowActions = useCallback( - (record: ClientAssessmentType) => { - return { - primaryAction: getViewAssessmentAction( - record, - clientId, - record.enrollment.id + 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] ); @@ -59,9 +67,7 @@ const ClientAssessmentsPage = () => { filters={filters} queryVariables={{ id: clientId }} queryDocument={GetClientAssessmentsDocument} - getTableRowActions={getTableRowActions} - getRowAccessibleName={(row) => assessmentDescription(row)} - columns={COLUMNS} + columns={columns} pagePath='client.assessments' fetchPolicy='cache-and-network' noData='No assessments' diff --git a/src/modules/bulkServices/components/BulkServicesTable.tsx b/src/modules/bulkServices/components/BulkServicesTable.tsx index d7ff11649..72676ceab 100644 --- a/src/modules/bulkServices/components/BulkServicesTable.tsx +++ b/src/modules/bulkServices/components/BulkServicesTable.tsx @@ -1,12 +1,14 @@ -import { ReactNode, useCallback, useMemo, useState } from 'react'; +import { ReactNode, useMemo, useState } from 'react'; import { ServicePeriod } from '../bulkServicesTypes'; import AssignServiceButton from './AssignServiceButton'; import MultiAssignServiceButton from './MultiAssignServiceButton'; import NotCollectedText from '@/components/elements/NotCollectedText'; +import TableRowActions from '@/components/elements/table/TableRowActions'; import { + BASE_ACTION_COLUMN_DEF, getViewClientAction, getViewEnrollmentAction, -} from '@/components/elements/table/tableActions/tableRowActionUtil'; +} 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'; @@ -101,36 +103,44 @@ const BulkServicesTable: React.FC = ({ return `${relative} (${formatted})`; }, }, - ] as ColumnDef[]; - }, [canViewDob, serviceDate, serviceTypeName]); - - const getTableRowActions = useCallback( - (record: RowType, loading?: boolean) => { - return { - primaryAction: ( - ( + } - serviceTypeName={serviceTypeName} + secondaryActionConfigs={[ + getViewClientAction(row), + ...(row.activeEnrollment + ? [getViewEnrollmentAction(row.activeEnrollment, row)] + : []), + ]} /> ), - secondaryActions: [ - getViewClientAction(record), - ...(record.activeEnrollment - ? [getViewEnrollmentAction(record.activeEnrollment, record)] - : []), - ], - }; - }, - [anyRowsSelected, mutationQueryVariables, serviceTypeName] - ); + }, + ] as ColumnDef[]; + }, [ + anyRowsSelected, + canViewDob, + mutationQueryVariables, + serviceDate, + serviceTypeName, + ]); const defaultFilterValues = useMemo(() => { if (!servicePeriod) return; @@ -169,8 +179,6 @@ const BulkServicesTable: React.FC = ({ queryDocument={BulkServicesClientSearchDocument} pagePath='clientSearch' columns={columns} - getTableRowActions={getTableRowActions} - getRowAccessibleName={(record: RowType) => clientBriefName(record)} recordType='Client' // TODO: add user-facing filter options for enrolled clients and bed night date. No filter options for now. defaultFilterValues={defaultFilterValues} diff --git a/src/modules/caseNotes/components/ClientCaseNotes.tsx b/src/modules/caseNotes/components/ClientCaseNotes.tsx index 965b35c61..152e91f33 100644 --- a/src/modules/caseNotes/components/ClientCaseNotes.tsx +++ b/src/modules/caseNotes/components/ClientCaseNotes.tsx @@ -1,8 +1,9 @@ import { Paper } from '@mui/material'; -import { useCallback } from 'react'; +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'; @@ -20,17 +21,6 @@ type Row = NonNullable< GetClientCaseNotesQuery['client'] >['customCaseNotes']['nodes'][0]; -const COLUMNS: ColumnDef[] = [ - CASE_NOTE_COLUMNS.InformationDate, - { - key: 'project', - header: 'Project Name', - render: (row) => row.enrollment.projectName, - }, - CASE_NOTE_COLUMNS.LastUpdated, - CASE_NOTE_COLUMNS.NoteContentPreview, -]; - const ClientCaseNotes = () => { const { client } = useClientDashboardContext(); @@ -44,16 +34,31 @@ const ClientCaseNotes = () => { maxWidth: 'sm', }); - const getTableRowActions = useCallback( - (record: Row) => { - return { - primaryAction: { - title: 'View Case Note', - key: 'case note', - onClick: () => onSelectRecord(record), - }, - }; - }, + 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] ); @@ -70,11 +75,7 @@ const ClientCaseNotes = () => { > queryVariables={{ id: clientId }} queryDocument={GetClientCaseNotesDocument} - columns={COLUMNS} - getTableRowActions={getTableRowActions} - getRowAccessibleName={(row) => - `${parseAndFormatDate(row.informationDate)} at ${row.enrollment.projectName}` - } + columns={columns} pagePath='client.customCaseNotes' noData='No case notes' headerCellSx={() => ({ color: 'text.secondary' })} diff --git a/src/modules/caseNotes/components/EnrollmentCaseNotes.tsx b/src/modules/caseNotes/components/EnrollmentCaseNotes.tsx index fbcc857f5..4c4bcddff 100644 --- a/src/modules/caseNotes/components/EnrollmentCaseNotes.tsx +++ b/src/modules/caseNotes/components/EnrollmentCaseNotes.tsx @@ -4,6 +4,8 @@ import { Box, Button } from '@mui/material'; import { useCallback } from 'react'; import { useViewEditRecordDialogs } from '../../form/hooks/useViewEditRecordDialogs'; +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'; @@ -108,25 +110,31 @@ 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 getTableRowActions = useCallback( - (record: CustomCaseNoteFieldsFragment) => { - return { - primaryAction: { - title: 'View Case Note', - key: 'case note', - onClick: () => onSelectRecord(record), + 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] ); @@ -165,10 +173,6 @@ const EnrollmentCaseNotes = () => { queryVariables={{ id: enrollmentId }} queryDocument={GetEnrollmentCustomCaseNotesDocument} getColumnDefs={getColumnDefs} - getTableRowActions={getTableRowActions} - getRowAccessibleName={(row) => - parseAndFormatDate(row.informationDate) || row.id - } pagePath='enrollment.customCaseNotes' noData='No case notes' headerCellSx={() => ({ color: 'text.secondary' })} diff --git a/src/modules/clientFiles/components/ClientFilesPage.tsx b/src/modules/clientFiles/components/ClientFilesPage.tsx index 6919a26d3..56fa5ecc2 100644 --- a/src/modules/clientFiles/components/ClientFilesPage.tsx +++ b/src/modules/clientFiles/components/ClientFilesPage.tsx @@ -1,11 +1,13 @@ import UploadIcon from '@mui/icons-material/Upload'; import { Box, Chip, Paper, Typography } from '@mui/material'; -import { useCallback, useMemo, useState } from 'react'; +import { useMemo, useState } from 'react'; import useFileActions from '../hooks/useFileActions'; import ButtonLink from '@/components/elements/ButtonLink'; import NotCollectedText from '@/components/elements/NotCollectedText'; +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'; @@ -124,24 +126,27 @@ const ClientFilesPage = () => { return `Unknown time ${byUser}`; }, }, + { + ...BASE_ACTION_COLUMN_DEF, + render: (file) => ( + setViewingFile(file), + } + } + /> + ), + }, ]; }, [pickListData]); - const getTableRowActions = useCallback( - (file: ClientFileType) => { - return file.redacted - ? {} - : { - primaryAction: { - title: 'View File', - key: 'file', - onClick: () => setViewingFile(file), - }, - }; - }, - [setViewingFile] - ); - return ( <> { queryVariables={{ id: clientId }} queryDocument={GetClientFilesDocument} columns={columns} - getTableRowActions={getTableRowActions} - getRowAccessibleName={(record) => record.name} pagePath='client.files' noData='No files' /> diff --git a/src/modules/dataFetching/components/GenericTableWithData.tsx b/src/modules/dataFetching/components/GenericTableWithData.tsx index 055bb5b61..04493276f 100644 --- a/src/modules/dataFetching/components/GenericTableWithData.tsx +++ b/src/modules/dataFetching/components/GenericTableWithData.tsx @@ -326,7 +326,7 @@ const GenericTableWithData = < loading={loading && !data} - loadingRegardlessOfData={loading} + //loadingRegardlessOfData={loading} // todo @martha - fix rows={rows} paginated={!nonTablePagination && !hidePagination} tablePaginationProps={ diff --git a/src/modules/enrollment/components/EnrollmentAssessmentsTable.tsx b/src/modules/enrollment/components/EnrollmentAssessmentsTable.tsx index ecb940d5a..1fdaa689f 100644 --- a/src/modules/enrollment/components/EnrollmentAssessmentsTable.tsx +++ b/src/modules/enrollment/components/EnrollmentAssessmentsTable.tsx @@ -1,7 +1,10 @@ -import { useCallback } from 'react'; +import { useMemo } from 'react'; -import { getViewAssessmentAction } from '@/components/elements/table/tableActions/tableRowActionUtil'; -import { ColumnDef } from '@/components/elements/table/types'; +import TableRowActions from '@/components/elements/table/TableRowActions'; +import { + BASE_ACTION_COLUMN_DEF, + getViewAssessmentAction, +} from '@/components/elements/table/tableRowActionUtil'; import { ASSESSMENT_COLUMNS } from '@/modules/assessments/util'; import GenericTableWithData from '@/modules/dataFetching/components/GenericTableWithData'; import { useFilters } from '@/modules/hmis/filterUtil'; @@ -13,12 +16,6 @@ import { GetEnrollmentAssessmentsQueryVariables, } from '@/types/gqlTypes'; -const COLUMNS: ColumnDef[] = [ - ASSESSMENT_COLUMNS.date, - ASSESSMENT_COLUMNS.type, - ASSESSMENT_COLUMNS.lastUpdated, -]; - interface Props { enrollmentId: string; clientId: string; @@ -30,16 +27,26 @@ const EnrollmentAssessmentsTable: React.FC = ({ enrollmentId, projectId, }) => { - const getTableRowActions = useCallback( - (assessment: AssessmentFieldsFragment) => { - return { - primaryAction: getViewAssessmentAction( - assessment, - clientId, - enrollmentId + const columns = useMemo( + () => [ + ASSESSMENT_COLUMNS.date, + ASSESSMENT_COLUMNS.type, + ASSESSMENT_COLUMNS.lastUpdated, + { + ...BASE_ACTION_COLUMN_DEF, + render: (assessment) => ( + ), - }; - }, + }, + ], [clientId, enrollmentId] ); @@ -57,9 +64,7 @@ const EnrollmentAssessmentsTable: React.FC = ({ filters={filters} queryVariables={{ id: enrollmentId }} queryDocument={GetEnrollmentAssessmentsDocument} - columns={COLUMNS} - getTableRowActions={getTableRowActions} - getRowAccessibleName={(record) => assessmentDescription(record)} + columns={columns} pagePath='enrollment.assessments' noData='No assessments' recordType='Assessment' diff --git a/src/modules/enrollment/components/HouseholdAssessmentsTable.tsx b/src/modules/enrollment/components/HouseholdAssessmentsTable.tsx index 7db917374..94333f448 100644 --- a/src/modules/enrollment/components/HouseholdAssessmentsTable.tsx +++ b/src/modules/enrollment/components/HouseholdAssessmentsTable.tsx @@ -1,4 +1,8 @@ -import { getViewAssessmentAction } from '@/components/elements/table/tableActions/tableRowActionUtil'; +import TableRowActions from '@/components/elements/table/TableRowActions'; +import { + BASE_ACTION_COLUMN_DEF, + getViewAssessmentAction, +} from '@/components/elements/table/tableRowActionUtil'; import { ColumnDef } from '@/components/elements/table/types'; import { ASSESSMENT_CLIENT_NAME_COL, @@ -6,7 +10,10 @@ import { } from '@/modules/assessments/util'; import GenericTableWithData from '@/modules/dataFetching/components/GenericTableWithData'; import { useFilters } from '@/modules/hmis/filterUtil'; -import { assessmentDescription } from '@/modules/hmis/hmisUtil'; +import { + assessmentDescription, + clientBriefName, +} from '@/modules/hmis/hmisUtil'; import { GetHouseholdAssessmentsDocument, GetHouseholdAssessmentsQuery, @@ -22,17 +29,25 @@ const COLUMNS: ColumnDef[] = [ ASSESSMENT_COLUMNS.date, ASSESSMENT_COLUMNS.type, ASSESSMENT_COLUMNS.lastUpdated, -]; - -const getTableRowActions = (assessment: HhmAssessmentType) => { - return { - primaryAction: getViewAssessmentAction( - assessment, - assessment.enrollment.client.id, - assessment.enrollment.id + { + ...BASE_ACTION_COLUMN_DEF, + render: (assessment: HhmAssessmentType) => ( + ), - }; -}; + }, +]; interface Props { householdId: string; @@ -58,8 +73,6 @@ const HouseholdAssessmentsTable: React.FC = ({ queryVariables={{ id: householdId }} queryDocument={GetHouseholdAssessmentsDocument} columns={COLUMNS} - getTableRowActions={getTableRowActions} - getRowAccessibleName={(record) => assessmentDescription(record)} 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 fd65c8f93..1f5f654ec 100644 --- a/src/modules/enrollment/components/pages/ClientEnrollmentsPage.tsx +++ b/src/modules/enrollment/components/pages/ClientEnrollmentsPage.tsx @@ -1,8 +1,12 @@ import { Paper, Stack, Typography } from '@mui/material'; -import { ReactNode, useCallback } from 'react'; +import { ReactNode, useMemo } from 'react'; import NotCollectedText from '@/components/elements/NotCollectedText'; -import { getViewEnrollmentAction } from '@/components/elements/table/tableActions/tableRowActionUtil'; +import TableRowActions from '@/components/elements/table/TableRowActions'; +import { + BASE_ACTION_COLUMN_DEF, + getViewEnrollmentAction, +} 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'; @@ -93,16 +97,6 @@ const CLIENT_ENROLLMENT_COLUMNS: { }, }; -const COLUMNS: ColumnDef[] = [ - 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, -]; - const ClientEnrollmentsPage = () => { const { client } = useClientDashboardContext(); @@ -110,16 +104,30 @@ const ClientEnrollmentsPage = () => { type: 'EnrollmentsForClientFilterOptions', }); - const getTableRowActions = useCallback( - (enrollment: ClientEnrollmentFieldsFragment) => { - return { - primaryAction: { - ...getViewEnrollmentAction(enrollment, client), - // override the default ariaLabel to provide the project name, since we are in the client context - ariaLabel: `View Enrollment at ${enrollment.projectName} for ${entryExitRange(enrollment)}`, - }, - }; - }, + 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] ); @@ -134,11 +142,7 @@ const ClientEnrollmentsPage = () => { > queryVariables={{ id: client.id }} queryDocument={GetClientEnrollmentsDocument} - columns={COLUMNS} - getTableRowActions={getTableRowActions} - getRowAccessibleName={(row) => - `${row.projectName} ${entryExitRange(row)}` - } + columns={columns} pagePath='client.enrollments' filters={filters} recordType='Enrollment' diff --git a/src/modules/enrollment/components/pages/EnrollmentCeAssessmentsPage.tsx b/src/modules/enrollment/components/pages/EnrollmentCeAssessmentsPage.tsx index 57dee4f84..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'; @@ -22,38 +24,6 @@ import { RecordFormRole, } from '@/types/gqlTypes'; -const COLUMNS: ColumnDef[] = [ - { - header: 'Assessment Date', - render: (a) => parseAndFormatDate(a.assessmentDate), - }, - { - header: 'Assessment Level', - render: (a) => ( - - ), - }, - { - header: 'Assessment Type', - render: (a) => ( - - ), - }, - { - header: 'Assessment Location', - render: (a) => a.assessmentLocation, - }, - { - header: 'Prioritization Status', - render: (a) => ( - - ), - }, -]; - const EnrollmentCeAssessmentsPage = () => { const { enrollment, getEnrollmentFeature } = useEnrollmentDashboardContext(); const enrollmentId = enrollment?.id; @@ -95,19 +65,66 @@ const EnrollmentCeAssessmentsPage = () => { DataCollectionFeatureRole.CeAssessment ); - const getTableRowActions = useCallback( - (record: CeAssessmentFieldsFragment) => { - return canEditCeAssessments - ? { - primaryAction: { - title: 'View CE Assessment', - key: 'ce assessment', - onClick: () => onSelectRecord(record), + const columns: ColumnDef[] = useMemo( + () => [ + { + header: 'Assessment Date', + render: (a: CeAssessmentFieldsFragment) => + parseAndFormatDate(a.assessmentDate), + }, + { + header: 'Assessment Level', + render: (a: CeAssessmentFieldsFragment) => ( + + ), + }, + { + header: 'Assessment Type', + render: (a: CeAssessmentFieldsFragment) => ( + + ), + }, + { + header: 'Assessment Location', + render: (a: CeAssessmentFieldsFragment) => a.assessmentLocation, + }, + { + header: 'Prioritization Status', + render: (a: CeAssessmentFieldsFragment) => ( + + ), + }, + ...(canEditCeAssessments + ? [ + { + ...BASE_ACTION_COLUMN_DEF, + render: (a: CeAssessmentFieldsFragment) => ( + onSelectRecord(a), + }} + /> + ), }, - } - : {}; - }, - [onSelectRecord, canEditCeAssessments] + ] + : []), + ], + [canEditCeAssessments, onSelectRecord] ); if (!enrollment || !enrollmentId || !clientId || !ceAssessmentFeature) @@ -139,11 +156,7 @@ const EnrollmentCeAssessmentsPage = () => { > queryVariables={{ id: enrollmentId }} queryDocument={GetEnrollmentCeAssessmentsDocument} - columns={COLUMNS} - getTableRowActions={getTableRowActions} - getRowAccessibleName={(record) => - parseAndFormatDate(record.assessmentDate) || 'unknown date' - } + columns={columns} pagePath='enrollment.ceAssessments' noData='No Coordinated Entry Assessments' headerCellSx={() => ({ color: 'text.secondary' })} diff --git a/src/modules/enrollment/components/pages/EnrollmentCeEventsPage.tsx b/src/modules/enrollment/components/pages/EnrollmentCeEventsPage.tsx index 0dd6d1cb0..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,21 +26,6 @@ import { RecordFormRole, } from '@/types/gqlTypes'; -const COLUMNS: ColumnDef[] = [ - { - header: 'Event Type', - render: (e) => , - }, - { - header: 'Event Date', - render: (e) => parseAndFormatDate(e.eventDate), - }, - { - header: 'Referral Result', - render: (e) => eventReferralResult(e), - }, -]; - const EnrollmentCeEventsPage = () => { const { enrollment, getEnrollmentFeature } = useEnrollmentDashboardContext(); const enrollmentId = enrollment?.id; @@ -80,16 +66,37 @@ const EnrollmentCeEventsPage = () => { DataCollectionFeatureRole.CeEvent ); - const getTableRowActions = useCallback( - (record: EventFieldsFragment) => { - return { - primaryAction: { - title: 'View CE Event', - key: 'ce event', - onClick: () => onSelectRecord(record), - }, - }; - }, + 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] ); @@ -122,11 +129,7 @@ const EnrollmentCeEventsPage = () => { > queryVariables={{ id: enrollmentId }} queryDocument={GetEnrollmentEventsDocument} - columns={COLUMNS} - getTableRowActions={getTableRowActions} - getRowAccessibleName={(record) => - `${HmisEnums.EventType[record.event]} on ${parseAndFormatDate(record.eventDate)}` - } + columns={columns} pagePath='enrollment.events' noData='No events' headerCellSx={() => ({ color: 'text.secondary' })} diff --git a/src/modules/enrollment/components/pages/EnrollmentCurrentLivingSituationsPage.tsx b/src/modules/enrollment/components/pages/EnrollmentCurrentLivingSituationsPage.tsx index 925367e7c..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'; @@ -98,21 +100,24 @@ const EnrollmentCurrentLivingSituationsPage = () => { CLS_COLUMNS.livingSituation, CLS_COLUMNS.locationDetails, ...customColumns, - ]; - }, - [] - ); - - const getTableRowActions = useCallback( - (record: CurrentLivingSituationFieldsFragment) => { - return { - primaryAction: { - title: 'View CLS', - key: 'cls', - ariaLabel: `View Current Living Situation, ${parseAndFormatDate(record.informationDate) || 'unknown date'}`, - onClick: () => onSelectRecord(record), + { + ...BASE_ACTION_COLUMN_DEF, + render: (cls: CurrentLivingSituationFieldsFragment) => ( + onSelectRecord(cls), + }} + /> + ), }, - }; + ]; }, [onSelectRecord] ); @@ -146,10 +151,6 @@ const EnrollmentCurrentLivingSituationsPage = () => { queryVariables={{ id: enrollmentId }} queryDocument={GetEnrollmentCurrentLivingSituationsDocument} getColumnDefs={getColumnDefs} - getTableRowActions={getTableRowActions} - getRowAccessibleName={(record) => - parseAndFormatDate(record.informationDate) || 'unknown date' - } pagePath='enrollment.currentLivingSituations' noData='No current living situations' recordType='CurrentLivingSituation' 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/projectAdministration/components/AllProjectsPage.tsx b/src/modules/projectAdministration/components/AllProjectsPage.tsx index c1a39b27f..8d8e70582 100644 --- a/src/modules/projectAdministration/components/AllProjectsPage.tsx +++ b/src/modules/projectAdministration/components/AllProjectsPage.tsx @@ -7,6 +7,8 @@ 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'; @@ -57,20 +59,24 @@ const PROJECT_COLUMNS: ColumnDef[] = [ project.operatingEndDate ), }, + { + ...BASE_ACTION_COLUMN_DEF, + render: (project: ProjectAllFieldsFragment) => ( + + ), + }, ]; -const getProjectTableActions = (project: ProjectAllFieldsFragment) => { - return { - primaryAction: { - title: 'View Project', - key: 'project', - to: generateSafePath(Routes.PROJECT, { - projectId: project.id, - }), - }, - }; -}; - const ORGANIZATION_COLUMNS: ColumnDef[] = [ { header: 'Organization Name', @@ -80,6 +86,22 @@ const ORGANIZATION_COLUMNS: ColumnDef[] = [ header: 'Project Count', render: 'projects.nodesCount' as keyof OrganizationType, }, + { + ...BASE_ACTION_COLUMN_DEF, + render: (organization: OrganizationType) => ( + + ), + }, ]; export type ViewMode = 'projects' | 'organizations'; @@ -96,18 +118,6 @@ const toggleItemDefinitions: ToggleItem[] = [ }, ]; -const getOrganizationTableActions = (organization: OrganizationType) => { - return { - primaryAction: { - title: 'View Organization', - key: 'organization', - to: generateSafePath(Routes.ORGANIZATION, { - organizationId: organization.id, - }), - }, - }; -}; - const ProjectsTable = ({ search, setSearch, @@ -148,8 +158,6 @@ const ProjectsTable = ({ defaultSortOption={ProjectSortOption.OrganizationAndName} queryDocument={GetProjectsDocument} columns={PROJECT_COLUMNS} - getTableRowActions={getProjectTableActions} - getRowAccessibleName={(project) => project.projectName} noData='No projects' pagePath='projects' recordType='Project' @@ -217,8 +225,6 @@ const OrganizationsTable = ({ key='organizationTable' queryDocument={GetOrganizationsDocument} columns={ORGANIZATION_COLUMNS} - getTableRowActions={getOrganizationTableActions} - getRowAccessibleName={(org) => org.organizationName} noData='No organizations' pagePath='organizations' recordType='Organization' diff --git a/src/modules/projects/components/ProjectAssessments.tsx b/src/modules/projects/components/ProjectAssessments.tsx index d59df9658..5855fae43 100644 --- a/src/modules/projects/components/ProjectAssessments.tsx +++ b/src/modules/projects/components/ProjectAssessments.tsx @@ -1,11 +1,12 @@ import { Paper } from '@mui/material'; -import { useCallback } from 'react'; +import { useMemo } from 'react'; import { useProjectDashboardContext } from './ProjectDashboard'; +import TableRowActions from '@/components/elements/table/TableRowActions'; import { + BASE_ACTION_COLUMN_DEF, getViewAssessmentAction, getViewEnrollmentAction, -} from '@/components/elements/table/tableActions/tableRowActionUtil'; -import { ColumnDef } from '@/components/elements/table/types'; +} from '@/components/elements/table/tableRowActionUtil'; import PageTitle from '@/components/layout/PageTitle'; import useSafeParams from '@/hooks/useSafeParams'; import { @@ -27,38 +28,44 @@ export type ProjectAssessmentType = NonNullable< GetProjectAssessmentsQuery['project'] >['assessments']['nodes'][number]; -const COLUMNS: ColumnDef[] = [ - ASSESSMENT_CLIENT_NAME_COL, - ASSESSMENT_COLUMNS.date, - ASSESSMENT_COLUMNS.type, - WITH_ENROLLMENT_COLUMNS.entryDate, - WITH_ENROLLMENT_COLUMNS.exitDate, -]; - const ProjectAssessments = () => { const { projectId } = useSafeParams() as { projectId: string; }; const { project } = useProjectDashboardContext(); - const getTableRowActions = useCallback( - (record: ProjectAssessmentType) => { - return { - primaryAction: { - ...getViewAssessmentAction( - record, - record.enrollment.client.id, - record.enrollment.id - ), - state: { backToLabel: project.projectName }, - }, - secondaryActions: [ - getViewEnrollmentAction(record.enrollment, record.enrollment.client), - ], - }; - }, - [project] - ); + const columns = useMemo(() => { + return [ + ASSESSMENT_CLIENT_NAME_COL, + ASSESSMENT_COLUMNS.date, + ASSESSMENT_COLUMNS.type, + WITH_ENROLLMENT_COLUMNS.entryDate, + WITH_ENROLLMENT_COLUMNS.exitDate, + { + ...BASE_ACTION_COLUMN_DEF, + render: (record: ProjectAssessmentType) => ( + + ), + }, + ]; + }, [project]); const filters = useFilters({ type: 'AssessmentsForProjectFilterOptions', @@ -76,9 +83,7 @@ const ProjectAssessments = () => { > queryVariables={{ id: projectId }} queryDocument={GetProjectAssessmentsDocument} - getTableRowActions={getTableRowActions} - getRowAccessibleName={(record) => assessmentDescription(record)} - columns={COLUMNS} + 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 85ffff60f..1e305a64e 100644 --- a/src/modules/projects/components/ProjectCurrentLivingSituations.tsx +++ b/src/modules/projects/components/ProjectCurrentLivingSituations.tsx @@ -1,5 +1,9 @@ import { Paper } from '@mui/material'; -import { getViewEnrollmentAction } from '@/components/elements/table/tableActions/tableRowActionUtil'; +import TableRowActions from '@/components/elements/table/TableRowActions'; +import { + BASE_ACTION_COLUMN_DEF, + getViewEnrollmentAction, +} from '@/components/elements/table/tableRowActionUtil'; import { ColumnDef } from '@/components/elements/table/types'; import PageTitle from '@/components/layout/PageTitle'; import useSafeParams from '@/hooks/useSafeParams'; @@ -28,27 +32,31 @@ const COLUMNS: ColumnDef[] = [ CLS_COLUMNS.livingSituation, WITH_ENROLLMENT_COLUMNS.entryDate, WITH_ENROLLMENT_COLUMNS.exitDate, + { + ...BASE_ACTION_COLUMN_DEF, + render: (cls) => ( + + ), + }, ]; -const getTableRowActions = ( - cls: ProjectCurrentLivingSituationFieldsFragment -) => { - return { - primaryAction: { - title: 'View CLS', - key: 'cls', - to: generateSafePath( - EnrollmentDashboardRoutes.CURRENT_LIVING_SITUATIONS, - { - clientId: cls.client.id, - enrollmentId: cls.enrollment.id, - } - ), - }, - secondaryActions: [getViewEnrollmentAction(cls.enrollment, cls.client)], - }; -}; - const ProjectCurrentLivingSituations = () => { const { projectId } = useSafeParams() as { projectId: string; @@ -66,10 +74,6 @@ const ProjectCurrentLivingSituations = () => { queryVariables={{ id: projectId }} queryDocument={GetProjectCurrentLivingSituationsDocument} columns={COLUMNS} - getTableRowActions={getTableRowActions} - getRowAccessibleName={(record) => - `${clientBriefName(record.client)} ${parseAndFormatDate(record.informationDate) || 'unknown date'}` - } pagePath='project.currentLivingSituations' noData='No current living situations' recordType='CurrentLivingSituation' diff --git a/src/modules/projects/components/tables/ProjectClientEnrollmentsTable.tsx b/src/modules/projects/components/tables/ProjectClientEnrollmentsTable.tsx index eebd10678..3bbda5d04 100644 --- a/src/modules/projects/components/tables/ProjectClientEnrollmentsTable.tsx +++ b/src/modules/projects/components/tables/ProjectClientEnrollmentsTable.tsx @@ -108,8 +108,9 @@ export const ASSIGNED_STAFF_COL = { type WithEnrollment = { enrollment: Pick< EnrollmentFieldsFragment, - 'entryDate' | 'exitDate' | 'inProgress' | 'autoExited' - >; + 'entryDate' | 'exitDate' | 'inProgress' + > & + Partial>; }; export const WITH_ENROLLMENT_COLUMNS: { [key: string]: ColumnDef; diff --git a/src/modules/projects/components/tables/ProjectExternalSubmissionsTable.tsx b/src/modules/projects/components/tables/ProjectExternalSubmissionsTable.tsx index 2dd6e7b3f..54f80aff7 100644 --- a/src/modules/projects/components/tables/ProjectExternalSubmissionsTable.tsx +++ b/src/modules/projects/components/tables/ProjectExternalSubmissionsTable.tsx @@ -2,6 +2,8 @@ import { Chip, Stack, Typography } from '@mui/material'; import { capitalize } from 'lodash-es'; import { useCallback, useState } from 'react'; import LoadingButton from '@/components/elements/LoadingButton'; +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 theme from '@/config/theme'; import GenericTableWithData from '@/modules/dataFetching/components/GenericTableWithData'; @@ -101,21 +103,22 @@ const ProjectExternalSubmissionsTable = ({ ), }, ...defs, - ]; - }, - [] - ); - - const getTableRowActions = useCallback( - ({ id }: ExternalFormSubmissionSummaryFragment) => { - return { - primaryAction: { - title: 'View Submission', - key: 'submission', - onClick: () => setModalOpenId(id), - disabled: bulkLoading, + { + ...BASE_ACTION_COLUMN_DEF, + render: (submission: ExternalFormSubmissionSummaryFragment) => ( + setModalOpenId(submission.id), + disabled: bulkLoading, + }} + /> + ), }, - }; + ]; }, [setModalOpenId, bulkLoading] ); @@ -142,7 +145,6 @@ const ProjectExternalSubmissionsTable = ({ } queryDocument={GetProjectExternalFormSubmissionsDocument} getColumnDefs={getColumnDefs} - getTableRowActions={getTableRowActions} noData='No external form submissions' pagePath='project.externalFormSubmissions' recordType='ExternalFormSubmission' diff --git a/src/modules/search/components/ClientSearch.tsx b/src/modules/search/components/ClientSearch.tsx index 81c408c6f..6152f7a02 100644 --- a/src/modules/search/components/ClientSearch.tsx +++ b/src/modules/search/components/ClientSearch.tsx @@ -14,7 +14,11 @@ import ClientSearchTypeToggle, { SearchType } from './ClientSearchTypeToggle'; import ClientTextSearchForm from './ClientTextSearchForm'; import ButtonLink from '@/components/elements/ButtonLink'; import { externalIdColumn } from '@/components/elements/ExternalIdDisplay'; -import { getViewClientAction } from '@/components/elements/table/tableActions/tableRowActionUtil'; +import TableRowActions from '@/components/elements/table/TableRowActions'; +import { + BASE_ACTION_COLUMN_DEF, + getViewClientAction, +} from '@/components/elements/table/tableRowActionUtil'; import { ColumnDef } from '@/components/elements/table/types'; import ClientName from '@/modules/client/components/ClientName'; @@ -117,15 +121,19 @@ export const CLIENT_COLUMNS: { }, }; -const getSearchResultTableActions = (record: ClientFieldsFragment) => { - return { - primaryAction: getViewClientAction(record), - }; -}; - const SEARCH_RESULT_COLUMNS: ColumnDef[] = [ CLIENT_COLUMNS.name, CLIENT_COLUMNS.age, + { + ...BASE_ACTION_COLUMN_DEF, + render: (client) => ( + + ), + }, ]; /** @@ -278,8 +286,6 @@ const ClientSearch = () => { queryDocument={SearchClientsDocument} onCompleted={() => setHasSearched(true)} columns={columns} - getTableRowActions={getSearchResultTableActions} - getRowAccessibleName={(record) => clientBriefName(record)} pagePath='clientSearch' fetchPolicy='cache-and-network' filters={filters} diff --git a/src/modules/services/components/ClientServicesPage.tsx b/src/modules/services/components/ClientServicesPage.tsx index 5f42df1a8..08750563d 100644 --- a/src/modules/services/components/ClientServicesPage.tsx +++ b/src/modules/services/components/ClientServicesPage.tsx @@ -1,10 +1,12 @@ import { Paper } from '@mui/material'; -import { useCallback, useMemo } from 'react'; +import React, { useMemo } from 'react'; +import TableRowActions from '@/components/elements/table/TableRowActions'; import { + BASE_ACTION_COLUMN_DEF, getViewEnrollmentAction, getViewServiceAction, -} from '@/components/elements/table/tableActions/tableRowActionUtil'; +} from '@/components/elements/table/tableRowActionUtil'; import { ColumnDef } from '@/components/elements/table/types'; import PageTitle from '@/components/layout/PageTitle'; import useSafeParams from '@/hooks/useSafeParams'; @@ -50,39 +52,42 @@ const ClientServicesPage: React.FC<{ optional: true, defaultHidden: true, }, + { + ...BASE_ACTION_COLUMN_DEF, + render: (row) => ( + + ), + }, ] as ColumnDef[] ).filter((col) => { if (omitColumns.includes(col.key || '')) return false; return true; }), - [omitColumns] + [clientId, omitColumns] ); const filters = useFilters({ type: 'ServiceFilterOptions', }); - const getTableRowActions = useCallback( - (service: ServiceType) => { - return { - primaryAction: getViewServiceAction( - service, - service.enrollment.id, - clientId - ), - secondaryActions: [ - { - ...getViewEnrollmentAction(service.enrollment, { id: clientId }), - // override the default ariaLabel to provide the project name, since we are in the client context - ariaLabel: `View Enrollment at ${service.enrollment.projectName} for ${entryExitRange(service.enrollment)}`, - }, - ], - }; - }, - [clientId] - ); - return ( <> @@ -96,10 +101,6 @@ const ClientServicesPage: React.FC<{ queryVariables={{ id: clientId }} queryDocument={GetClientServicesDocument} columns={columns} - getTableRowActions={getTableRowActions} - getRowAccessibleName={(record) => - `${getServiceTypeForDisplay(record.serviceType)} on ${parseAndFormatDate(record.dateProvided)}` - } pagePath='client.services' fetchPolicy='cache-and-network' noData='No services' diff --git a/src/modules/services/components/EnrollmentServicesPage.tsx b/src/modules/services/components/EnrollmentServicesPage.tsx index e5d881179..3974830f9 100644 --- a/src/modules/services/components/EnrollmentServicesPage.tsx +++ b/src/modules/services/components/EnrollmentServicesPage.tsx @@ -1,7 +1,9 @@ import AddIcon from '@mui/icons-material/Add'; import { Button } from '@mui/material'; -import { useCallback, 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'; @@ -22,12 +24,6 @@ import { ServiceFieldsFragment, } from '@/types/gqlTypes'; -const COLUMNS = [ - SERVICE_BASIC_COLUMNS.serviceDate, - SERVICE_BASIC_COLUMNS.serviceType, - SERVICE_COLUMNS.serviceDetails, -]; - const EnrollmentServicesPage = () => { const { enrollment, getEnrollmentFeature } = useEnrollmentDashboardContext(); const enrollmentId = enrollment?.id; @@ -53,23 +49,34 @@ const EnrollmentServicesPage = () => { const canEditServices = enrollment?.access.canEditEnrollments; - const getTableRowActions = useCallback( - (service: ServiceFieldsFragment) => { - return canEditServices - ? { - primaryAction: { - title: 'Update Service', - key: 'service', - onClick: () => { - setViewingRecord(service); - openServiceDialog(); - }, + 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] - ); + ] + : []), + ]; + }, [canEditServices, openServiceDialog]); if (!enrollment || !enrollmentId || !clientId || !serviceFeature) return ; @@ -100,11 +107,7 @@ const EnrollmentServicesPage = () => { > queryVariables={{ id: enrollmentId }} queryDocument={GetEnrollmentServicesDocument} - columns={COLUMNS} - getTableRowActions={getTableRowActions} - getRowAccessibleName={(record) => - `${getServiceTypeForDisplay(record.serviceType)} on ${parseAndFormatDate(record.dateProvided)}` - } + columns={columns} pagePath='enrollment.services' noData='No services' recordType='Service' diff --git a/src/modules/services/components/ProjectServicesTable.tsx b/src/modules/services/components/ProjectServicesTable.tsx index e85e4356b..dd8225014 100644 --- a/src/modules/services/components/ProjectServicesTable.tsx +++ b/src/modules/services/components/ProjectServicesTable.tsx @@ -1,9 +1,11 @@ import { useMemo } from 'react'; +import TableRowActions from '@/components/elements/table/TableRowActions'; import { + BASE_ACTION_COLUMN_DEF, getViewEnrollmentAction, getViewServiceAction, -} from '@/components/elements/table/tableActions/tableRowActionUtil'; +} 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'; @@ -26,19 +28,6 @@ export type ServiceFields = NonNullable< GetProjectServicesQuery['project'] >['services']['nodes'][number]; -const getTableRowActions = (service: ServiceFields) => { - return { - primaryAction: getViewServiceAction( - service, - service.enrollment.id, - service.enrollment.client.id - ), - secondaryActions: [ - getViewEnrollmentAction(service.enrollment, service.enrollment.client), - ], - }; -}; - const ProjectServicesTable = ({ projectId, columns, @@ -59,6 +48,26 @@ const ProjectServicesTable = ({ SERVICE_BASIC_COLUMNS.serviceType, WITH_ENROLLMENT_COLUMNS.entryDate, WITH_ENROLLMENT_COLUMNS.exitDate, + { + ...BASE_ACTION_COLUMN_DEF, + render: (service: ServiceFields) => ( + + ), + }, ]; }, [columns]); @@ -78,10 +87,6 @@ const ProjectServicesTable = ({ }} queryDocument={GetProjectServicesDocument} columns={displayColumns} - getTableRowActions={getTableRowActions} - getRowAccessibleName={(record) => - `${clientBriefName(record.enrollment.client)}'s ${getServiceTypeForDisplay(record.serviceType)} on ${parseAndFormatDate(record.dateProvided)}` - } noData='No services' pagePath='project.services' recordType='Service' From d5bd5aabbec903067c11da94f633d8d25f78b1d3 Mon Sep 17 00:00:00 2001 From: martha Date: Thu, 2 Jan 2025 12:53:54 -0500 Subject: [PATCH 07/13] Fix bulk services loading --- .../elements/table/TableRowActions.tsx | 2 - .../components/BulkServicesTable.tsx | 150 +++++++++--------- .../components/GenericTableWithData.tsx | 1 - 3 files changed, 77 insertions(+), 76 deletions(-) diff --git a/src/components/elements/table/TableRowActions.tsx b/src/components/elements/table/TableRowActions.tsx index 059dc7624..014a5d662 100644 --- a/src/components/elements/table/TableRowActions.tsx +++ b/src/components/elements/table/TableRowActions.tsx @@ -21,8 +21,6 @@ const TableRowActions = ({ primaryActionConfig, secondaryActionConfigs, }: TableRowActionsProps) => { - // todo @martha - confirm that secondary actions have correct aria, maybe require it on the type? - // proposal is to support alternative way of providing aria for primary, but secondaries its ok return ( {!!primaryActionConfig && primaryActionConfig.to && ( diff --git a/src/modules/bulkServices/components/BulkServicesTable.tsx b/src/modules/bulkServices/components/BulkServicesTable.tsx index 72676ceab..2515b9f37 100644 --- a/src/modules/bulkServices/components/BulkServicesTable.tsx +++ b/src/modules/bulkServices/components/BulkServicesTable.tsx @@ -1,4 +1,4 @@ -import { ReactNode, useMemo, useState } from 'react'; +import { ReactNode, useCallback, useMemo, useState } from 'react'; import { ServicePeriod } from '../bulkServicesTypes'; import AssignServiceButton from './AssignServiceButton'; import MultiAssignServiceButton from './MultiAssignServiceButton'; @@ -66,81 +66,85 @@ const BulkServicesTable: React.FC = ({ const [canViewDob] = useHasRootPermissions(['canViewDob']); - const columns = useMemo(() => { - const notEnrolledText = ( - - Not enrolled on {formatDateForDisplay(serviceDate, 'M/d')} - - ); - return [ - CLIENT_COLUMNS.name, - ...(canViewDob ? [CLIENT_COLUMNS.dobAge] : []), - { - header: 'Entry Date', - render: (row: RowType) => { - if (!row.activeEnrollment) return notEnrolledText; + const getColumnDefs = useCallback( + (_rows: RowType[], loading?: boolean) => { + const notEnrolledText = ( + + Not enrolled on {formatDateForDisplay(serviceDate, 'M/d')} + + ); + return [ + 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); + }, }, - }, - { - header: `Last ${serviceTypeName} Date`, - render: (row: RowType) => { - if (!row.activeEnrollment) return notEnrolledText; + { + header: `Last ${serviceTypeName} Date`, + render: (row: RowType) => { + if (!row.activeEnrollment) return notEnrolledText; - const noService = ( - - No Previous {serviceTypeName} - - ); - if (!row.activeEnrollment.lastServiceDate) { - return noService; - } - const dt = parseHmisDateString(row.activeEnrollment.lastServiceDate); - if (!dt) return noService; - const relative = formatRelativeDate(dt); - const formatted = formatDateForDisplay(dt); - return `${relative} (${formatted})`; - }, - }, - { - ...BASE_ACTION_COLUMN_DEF, - // todo @martha - loading not working - render: (row: RowType, loading: boolean) => ( - + const noService = ( + + No Previous {serviceTypeName} + + ); + if (!row.activeEnrollment.lastServiceDate) { + return noService; } - secondaryActionConfigs={[ - getViewClientAction(row), - ...(row.activeEnrollment - ? [getViewEnrollmentAction(row.activeEnrollment, row)] - : []), - ]} - /> - ), - }, - ] as ColumnDef[]; - }, [ - anyRowsSelected, - canViewDob, - mutationQueryVariables, - serviceDate, - serviceTypeName, - ]); + const dt = parseHmisDateString( + row.activeEnrollment.lastServiceDate + ); + if (!dt) return noService; + const relative = formatRelativeDate(dt); + const formatted = formatDateForDisplay(dt); + return `${relative} (${formatted})`; + }, + }, + { + ...BASE_ACTION_COLUMN_DEF, + render: (row: RowType) => ( + + } + secondaryActionConfigs={[ + getViewClientAction(row), + ...(row.activeEnrollment + ? [getViewEnrollmentAction(row.activeEnrollment, row)] + : []), + ]} + /> + ), + }, + ] as ColumnDef[]; + }, + [ + anyRowsSelected, + canViewDob, + mutationQueryVariables, + serviceDate, + serviceTypeName, + ] + ); const defaultFilterValues = useMemo(() => { if (!servicePeriod) return; @@ -178,7 +182,7 @@ const BulkServicesTable: React.FC = ({ onChangeSelectedRowIds={(rows) => setAnyRowsSelected(rows.length > 0)} queryDocument={BulkServicesClientSearchDocument} pagePath='clientSearch' - columns={columns} + getColumnDefs={getColumnDefs} recordType='Client' // TODO: add user-facing filter options for enrolled clients and bed night date. No filter options for now. defaultFilterValues={defaultFilterValues} diff --git a/src/modules/dataFetching/components/GenericTableWithData.tsx b/src/modules/dataFetching/components/GenericTableWithData.tsx index 04493276f..b62ea70b6 100644 --- a/src/modules/dataFetching/components/GenericTableWithData.tsx +++ b/src/modules/dataFetching/components/GenericTableWithData.tsx @@ -326,7 +326,6 @@ const GenericTableWithData = < loading={loading && !data} - //loadingRegardlessOfData={loading} // todo @martha - fix rows={rows} paginated={!nonTablePagination && !hidePagination} tablePaginationProps={ From e316ed087a579618778075cc86d6c7a893b39993 Mon Sep 17 00:00:00 2001 From: martha Date: Thu, 2 Jan 2025 13:05:50 -0500 Subject: [PATCH 08/13] Provide a visually hidden header when header isnt provided --- .../elements/table/GenericTable.tsx | 8 ++++++- .../elements/table/tableRowActionUtil.tsx | 4 ---- src/components/elements/table/types.tsx | 21 +++++++++++++------ .../admin/components/users/AdminUsers.tsx | 1 + 4 files changed, 23 insertions(+), 11 deletions(-) diff --git a/src/components/elements/table/GenericTable.tsx b/src/components/elements/table/GenericTable.tsx index fcef38866..da93a7a1c 100644 --- a/src/components/elements/table/GenericTable.tsx +++ b/src/components/elements/table/GenericTable.tsx @@ -17,6 +17,7 @@ import { TableRow, Theme, } from '@mui/material'; +import { visuallyHidden } from '@mui/utils'; import { compact, get, includes, isNil, without } from 'lodash-es'; import { ComponentType, @@ -242,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} + )} ))} diff --git a/src/components/elements/table/tableRowActionUtil.tsx b/src/components/elements/table/tableRowActionUtil.tsx index c04c211b2..2cec1f3d8 100644 --- a/src/components/elements/table/tableRowActionUtil.tsx +++ b/src/components/elements/table/tableRowActionUtil.tsx @@ -1,5 +1,3 @@ -import { Box } from '@mui/material'; -import { visuallyHidden } from '@mui/utils'; import { ColumnDef } from '@/components/elements/table/types'; import { generateAssessmentPath } from '@/modules/assessments/util'; import { @@ -21,9 +19,7 @@ import { } from '@/types/gqlTypes'; import { generateSafePath } from '@/utils/pathEncoding'; -// TODO(#6761) - enforce header-less columns always have accessible text in GenericTable export const BASE_ACTION_COLUMN_DEF: ColumnDef = { - header: Actions, key: 'Actions', tableCellProps: { sx: { py: 0 } }, render: '', // gets overridden when used 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/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 && ( From 004ea9945bd88bd3adaf1a26ba74d0084ed1a5b4 Mon Sep 17 00:00:00 2001 From: martha Date: Thu, 2 Jan 2025 13:48:17 -0500 Subject: [PATCH 09/13] Keep existing behavior of individual/household assessment viewing --- src/components/elements/table/tableRowActionUtil.tsx | 10 ++++++++-- .../components/pages/ClientAssessmentsPage.tsx | 3 ++- src/modules/projects/components/ProjectAssessments.tsx | 3 ++- 3 files changed, 12 insertions(+), 4 deletions(-) diff --git a/src/components/elements/table/tableRowActionUtil.tsx b/src/components/elements/table/tableRowActionUtil.tsx index 2cec1f3d8..00759e360 100644 --- a/src/components/elements/table/tableRowActionUtil.tsx +++ b/src/components/elements/table/tableRowActionUtil.tsx @@ -54,13 +54,19 @@ export const getViewEnrollmentAction = ( export const getViewAssessmentAction = ( assessment: AssessmentFieldsFragment, clientId: string, - enrollmentId: string + enrollmentId: string, + individualViewOnly?: boolean ) => { return { title: 'View Assessment', key: 'assessment', ariaLabel: `View Assessment, ${assessmentDescription(assessment)}`, - to: generateAssessmentPath(assessment, clientId, enrollmentId), + to: generateAssessmentPath( + assessment, + clientId, + enrollmentId, + individualViewOnly + ), }; }; diff --git a/src/modules/assessments/components/pages/ClientAssessmentsPage.tsx b/src/modules/assessments/components/pages/ClientAssessmentsPage.tsx index 7b873128c..5f0bcd968 100644 --- a/src/modules/assessments/components/pages/ClientAssessmentsPage.tsx +++ b/src/modules/assessments/components/pages/ClientAssessmentsPage.tsx @@ -46,7 +46,8 @@ const ClientAssessmentsPage = () => { primaryActionConfig={getViewAssessmentAction( row, clientId, - row.enrollment.id + row.enrollment.id, + true // open the assessment for individual viewing, even if it's an intake/exit in a multimember household )} /> ), diff --git a/src/modules/projects/components/ProjectAssessments.tsx b/src/modules/projects/components/ProjectAssessments.tsx index 5855fae43..9f42943ac 100644 --- a/src/modules/projects/components/ProjectAssessments.tsx +++ b/src/modules/projects/components/ProjectAssessments.tsx @@ -51,7 +51,8 @@ const ProjectAssessments = () => { ...getViewAssessmentAction( record, record.enrollment.client.id, - record.enrollment.id + record.enrollment.id, + true // open the assessment for individual viewing, even if it's an intake/exit in a multimember household ), linkState: { backToLabel: project.projectName }, }} From bdde16212656cba866e458366b6a73b18b2032c5 Mon Sep 17 00:00:00 2001 From: martha Date: Fri, 3 Jan 2025 10:46:11 -0500 Subject: [PATCH 10/13] Fix typescript --- src/components/elements/RelativeDateDisplay.tsx | 4 +++- src/components/elements/table/tableRowActionUtil.tsx | 8 ++++---- .../components/pages/ClientAssessmentsPage.tsx | 4 ++-- src/modules/assessments/util.tsx | 2 +- src/modules/bulkServices/components/BulkServicesTable.tsx | 8 ++++---- src/modules/caseNotes/components/EnrollmentCaseNotes.tsx | 2 +- src/modules/clientFiles/components/ClientFilesPage.tsx | 2 +- .../enrollment/components/EnrollmentAssessmentsTable.tsx | 4 ++-- .../enrollment/components/HouseholdAssessmentsTable.tsx | 4 ++-- .../enrollment/components/pages/ClientEnrollmentsPage.tsx | 4 ++-- src/modules/projects/components/ProjectAssessments.tsx | 8 ++++---- .../components/ProjectCurrentLivingSituations.tsx | 4 ++-- src/modules/search/components/ClientSearch.tsx | 4 ++-- src/modules/services/components/ClientServicesPage.tsx | 8 ++++---- src/modules/services/components/ProjectServicesTable.tsx | 8 ++++---- 15 files changed, 38 insertions(+), 36 deletions(-) diff --git a/src/components/elements/RelativeDateDisplay.tsx b/src/components/elements/RelativeDateDisplay.tsx index bbac95450..1ccd01fae 100644 --- a/src/components/elements/RelativeDateDisplay.tsx +++ b/src/components/elements/RelativeDateDisplay.tsx @@ -29,6 +29,7 @@ export interface RelativeDateDisplayProps { dateString: string; prefixVerb?: string; suffixText?: string; + tooltipSuffixText?: string; TooltipProps?: Omit; TypographyProps?: TypographyProps; } @@ -40,6 +41,7 @@ const RelativeDateDisplay = ({ dateString, prefixVerb, suffixText, + tooltipSuffixText, TooltipProps = {}, TypographyProps = {}, }: RelativeDateDisplayProps) => { @@ -54,7 +56,7 @@ const RelativeDateDisplay = ({ - {formattedDate} + {formattedDate} {tooltipSuffixText} } arrow diff --git a/src/components/elements/table/tableRowActionUtil.tsx b/src/components/elements/table/tableRowActionUtil.tsx index cf8e4684b..e26768981 100644 --- a/src/components/elements/table/tableRowActionUtil.tsx +++ b/src/components/elements/table/tableRowActionUtil.tsx @@ -38,12 +38,12 @@ export const getViewClientMenuItem = (client: ClientNameFragment) => { export const getViewEnrollmentMenuItem = ( enrollment: Pick, - client: ClientNameFragment + client: Pick | ClientNameFragment ) => { return { title: 'View Enrollment', key: 'enrollment', - ariaLabel: `View Enrollment, ${clientBriefName(client)} ${entryExitRange(enrollment)}`, + ariaLabel: `View Enrollment, ${client.hasOwnProperty('firstName') ? clientBriefName(client as ClientNameFragment) : ''} ${entryExitRange(enrollment)}`, to: generateSafePath(EnrollmentDashboardRoutes.ENROLLMENT_OVERVIEW, { clientId: client.id, enrollmentId: enrollment.id, @@ -51,7 +51,7 @@ export const getViewEnrollmentMenuItem = ( }; }; -export const getViewAssessmentAction = ( +export const getViewAssessmentMenuItem = ( assessment: AssessmentFieldsFragment, clientId: string, enrollmentId: string, @@ -70,7 +70,7 @@ export const getViewAssessmentAction = ( }; }; -export const getViewServiceAction = ( +export const getViewServiceMenuItem = ( service: Pick, enrollmentId: string, clientId: string diff --git a/src/modules/assessments/components/pages/ClientAssessmentsPage.tsx b/src/modules/assessments/components/pages/ClientAssessmentsPage.tsx index 5f0bcd968..bd3497cb7 100644 --- a/src/modules/assessments/components/pages/ClientAssessmentsPage.tsx +++ b/src/modules/assessments/components/pages/ClientAssessmentsPage.tsx @@ -4,7 +4,7 @@ import { useMemo } from 'react'; import TableRowActions from '@/components/elements/table/TableRowActions'; import { BASE_ACTION_COLUMN_DEF, - getViewAssessmentAction, + getViewAssessmentMenuItem, } from '@/components/elements/table/tableRowActionUtil'; import { ColumnDef } from '@/components/elements/table/types'; import PageTitle from '@/components/layout/PageTitle'; @@ -43,7 +43,7 @@ const ClientAssessmentsPage = () => { = ({ /> } secondaryActionConfigs={[ - getViewClientAction(row), + getViewClientMenuItem(row), ...(row.activeEnrollment - ? [getViewEnrollmentAction(row.activeEnrollment, row)] + ? [getViewEnrollmentMenuItem(row.activeEnrollment, row)] : []), ]} /> diff --git a/src/modules/caseNotes/components/EnrollmentCaseNotes.tsx b/src/modules/caseNotes/components/EnrollmentCaseNotes.tsx index 4c4bcddff..76225b3b4 100644 --- a/src/modules/caseNotes/components/EnrollmentCaseNotes.tsx +++ b/src/modules/caseNotes/components/EnrollmentCaseNotes.tsx @@ -4,13 +4,13 @@ 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 RelativeDateDisplay from '@/modules/hmis/components/RelativeDateDisplay'; import { getCustomDataElementColumns, parseAndFormatDate, diff --git a/src/modules/clientFiles/components/ClientFilesPage.tsx b/src/modules/clientFiles/components/ClientFilesPage.tsx index 56fa5ecc2..8175e541e 100644 --- a/src/modules/clientFiles/components/ClientFilesPage.tsx +++ b/src/modules/clientFiles/components/ClientFilesPage.tsx @@ -6,6 +6,7 @@ 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'; @@ -13,7 +14,6 @@ import FilePreviewDialog from '@/components/elements/upload/fileDialog/FilePrevi import PageTitle from '@/components/layout/PageTitle'; import useSafeParams from '@/hooks/useSafeParams'; import GenericTableWithData from '@/modules/dataFetching/components/GenericTableWithData'; -import RelativeDateDisplay from '@/modules/hmis/components/RelativeDateDisplay'; import { useClientPermissions, useHasClientPermissions, diff --git a/src/modules/enrollment/components/EnrollmentAssessmentsTable.tsx b/src/modules/enrollment/components/EnrollmentAssessmentsTable.tsx index 1fdaa689f..83462fa49 100644 --- a/src/modules/enrollment/components/EnrollmentAssessmentsTable.tsx +++ b/src/modules/enrollment/components/EnrollmentAssessmentsTable.tsx @@ -3,7 +3,7 @@ import { useMemo } from 'react'; import TableRowActions from '@/components/elements/table/TableRowActions'; import { BASE_ACTION_COLUMN_DEF, - getViewAssessmentAction, + getViewAssessmentMenuItem, } from '@/components/elements/table/tableRowActionUtil'; import { ASSESSMENT_COLUMNS } from '@/modules/assessments/util'; import GenericTableWithData from '@/modules/dataFetching/components/GenericTableWithData'; @@ -38,7 +38,7 @@ const EnrollmentAssessmentsTable: React.FC = ({ [] = [ record={assessment} recordName={assessmentDescription(assessment)} primaryActionConfig={{ - ...getViewAssessmentAction( + ...getViewAssessmentMenuItem( assessment, assessment.enrollment.client.id, assessment.enrollment.id diff --git a/src/modules/enrollment/components/pages/ClientEnrollmentsPage.tsx b/src/modules/enrollment/components/pages/ClientEnrollmentsPage.tsx index 1f5f654ec..1e63f7698 100644 --- a/src/modules/enrollment/components/pages/ClientEnrollmentsPage.tsx +++ b/src/modules/enrollment/components/pages/ClientEnrollmentsPage.tsx @@ -5,7 +5,7 @@ import NotCollectedText from '@/components/elements/NotCollectedText'; import TableRowActions from '@/components/elements/table/TableRowActions'; import { BASE_ACTION_COLUMN_DEF, - getViewEnrollmentAction, + getViewEnrollmentMenuItem, } from '@/components/elements/table/tableRowActionUtil'; import { ColumnDef } from '@/components/elements/table/types'; import PageTitle from '@/components/layout/PageTitle'; @@ -120,7 +120,7 @@ const ClientEnrollmentsPage = () => { record={enrollment} recordName={`${enrollment.projectName} ${entryExitRange(enrollment)}`} primaryActionConfig={{ - ...getViewEnrollmentAction(enrollment, client), + ...getViewEnrollmentMenuItem(enrollment, client), // override the default ariaLabel to provide the project name, since we are in the client context ariaLabel: `View Enrollment at ${enrollment.projectName} for ${entryExitRange(enrollment)}`, }} diff --git a/src/modules/projects/components/ProjectAssessments.tsx b/src/modules/projects/components/ProjectAssessments.tsx index 9f42943ac..5fde2713a 100644 --- a/src/modules/projects/components/ProjectAssessments.tsx +++ b/src/modules/projects/components/ProjectAssessments.tsx @@ -4,8 +4,8 @@ import { useProjectDashboardContext } from './ProjectDashboard'; import TableRowActions from '@/components/elements/table/TableRowActions'; import { BASE_ACTION_COLUMN_DEF, - getViewAssessmentAction, - getViewEnrollmentAction, + getViewAssessmentMenuItem, + getViewEnrollmentMenuItem, } from '@/components/elements/table/tableRowActionUtil'; import PageTitle from '@/components/layout/PageTitle'; import useSafeParams from '@/hooks/useSafeParams'; @@ -48,7 +48,7 @@ const ProjectAssessments = () => { record={record} recordName={assessmentDescription(record)} primaryActionConfig={{ - ...getViewAssessmentAction( + ...getViewAssessmentMenuItem( record, record.enrollment.client.id, record.enrollment.id, @@ -57,7 +57,7 @@ const ProjectAssessments = () => { linkState: { backToLabel: project.projectName }, }} secondaryActionConfigs={[ - getViewEnrollmentAction( + getViewEnrollmentMenuItem( record.enrollment, record.enrollment.client ), diff --git a/src/modules/projects/components/ProjectCurrentLivingSituations.tsx b/src/modules/projects/components/ProjectCurrentLivingSituations.tsx index 1e305a64e..2a3dd9374 100644 --- a/src/modules/projects/components/ProjectCurrentLivingSituations.tsx +++ b/src/modules/projects/components/ProjectCurrentLivingSituations.tsx @@ -2,7 +2,7 @@ import { Paper } from '@mui/material'; import TableRowActions from '@/components/elements/table/TableRowActions'; import { BASE_ACTION_COLUMN_DEF, - getViewEnrollmentAction, + getViewEnrollmentMenuItem, } from '@/components/elements/table/tableRowActionUtil'; import { ColumnDef } from '@/components/elements/table/types'; import PageTitle from '@/components/layout/PageTitle'; @@ -50,7 +50,7 @@ const COLUMNS: ColumnDef[] = [ ), }} secondaryActionConfigs={[ - getViewEnrollmentAction(cls.enrollment, cls.client), + getViewEnrollmentMenuItem(cls.enrollment, cls.client), ]} /> ), diff --git a/src/modules/search/components/ClientSearch.tsx b/src/modules/search/components/ClientSearch.tsx index 6152f7a02..6605fc75b 100644 --- a/src/modules/search/components/ClientSearch.tsx +++ b/src/modules/search/components/ClientSearch.tsx @@ -17,7 +17,7 @@ import { externalIdColumn } from '@/components/elements/ExternalIdDisplay'; import TableRowActions from '@/components/elements/table/TableRowActions'; import { BASE_ACTION_COLUMN_DEF, - getViewClientAction, + getViewClientMenuItem, } from '@/components/elements/table/tableRowActionUtil'; import { ColumnDef } from '@/components/elements/table/types'; @@ -130,7 +130,7 @@ const SEARCH_RESULT_COLUMNS: ColumnDef[] = [ ), }, diff --git a/src/modules/services/components/ClientServicesPage.tsx b/src/modules/services/components/ClientServicesPage.tsx index 08750563d..b06631895 100644 --- a/src/modules/services/components/ClientServicesPage.tsx +++ b/src/modules/services/components/ClientServicesPage.tsx @@ -4,8 +4,8 @@ import React, { useMemo } from 'react'; import TableRowActions from '@/components/elements/table/TableRowActions'; import { BASE_ACTION_COLUMN_DEF, - getViewEnrollmentAction, - getViewServiceAction, + getViewEnrollmentMenuItem, + getViewServiceMenuItem, } from '@/components/elements/table/tableRowActionUtil'; import { ColumnDef } from '@/components/elements/table/types'; import PageTitle from '@/components/layout/PageTitle'; @@ -58,14 +58,14 @@ const ClientServicesPage: React.FC<{ Date: Fri, 3 Jan 2025 10:54:26 -0500 Subject: [PATCH 11/13] Add accessible text for RelativeDateDisplay --- src/components/elements/RelativeDateDisplay.tsx | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/components/elements/RelativeDateDisplay.tsx b/src/components/elements/RelativeDateDisplay.tsx index 1ccd01fae..81c7ce60b 100644 --- a/src/components/elements/RelativeDateDisplay.tsx +++ b/src/components/elements/RelativeDateDisplay.tsx @@ -1,9 +1,11 @@ import { + Box, Tooltip, TooltipProps, Typography, TypographyProps, } from '@mui/material'; +import { visuallyHidden } from '@mui/utils'; import { useMemo } from 'react'; import { @@ -71,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} ); From eee782f11fd4276c955c381161b50d7340c10f3d Mon Sep 17 00:00:00 2001 From: martha Date: Mon, 6 Jan 2025 10:16:22 -0500 Subject: [PATCH 12/13] implement one possible approach for client ID typescript --- src/components/elements/table/tableRowActionUtil.tsx | 4 ++-- src/modules/services/components/ClientServicesPage.tsx | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/components/elements/table/tableRowActionUtil.tsx b/src/components/elements/table/tableRowActionUtil.tsx index e26768981..729aabc8c 100644 --- a/src/components/elements/table/tableRowActionUtil.tsx +++ b/src/components/elements/table/tableRowActionUtil.tsx @@ -38,12 +38,12 @@ export const getViewClientMenuItem = (client: ClientNameFragment) => { export const getViewEnrollmentMenuItem = ( enrollment: Pick, - client: Pick | ClientNameFragment + client: ClientNameFragment ) => { return { title: 'View Enrollment', key: 'enrollment', - ariaLabel: `View Enrollment, ${client.hasOwnProperty('firstName') ? clientBriefName(client as ClientNameFragment) : ''} ${entryExitRange(enrollment)}`, + ariaLabel: `View Enrollment, ${clientBriefName(client as ClientNameFragment)} ${entryExitRange(enrollment)}`, to: generateSafePath(EnrollmentDashboardRoutes.ENROLLMENT_OVERVIEW, { clientId: client.id, enrollmentId: enrollment.id, diff --git a/src/modules/services/components/ClientServicesPage.tsx b/src/modules/services/components/ClientServicesPage.tsx index b06631895..fb590d088 100644 --- a/src/modules/services/components/ClientServicesPage.tsx +++ b/src/modules/services/components/ClientServicesPage.tsx @@ -10,6 +10,7 @@ import { 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 { useFilters } from '@/modules/hmis/filterUtil'; @@ -35,6 +36,7 @@ const ClientServicesPage: React.FC<{ enrollmentId?: string; }> = ({ omitColumns = [] }) => { const { clientId } = useSafeParams() as { clientId: string }; + const { client } = useClientDashboardContext(); const columns = useMemo( () => @@ -65,9 +67,7 @@ const ClientServicesPage: React.FC<{ )} secondaryActionConfigs={[ { - ...getViewEnrollmentMenuItem(row.enrollment, { - id: clientId, - }), + ...getViewEnrollmentMenuItem(row.enrollment, client), // override the default ariaLabel to provide the project name, since we are in the client context ariaLabel: `View Enrollment at ${row.enrollment.projectName} for ${entryExitRange(row.enrollment)}`, }, @@ -81,7 +81,7 @@ const ClientServicesPage: React.FC<{ return true; }), - [clientId, omitColumns] + [client, clientId, omitColumns] ); const filters = useFilters({ From 61499cfbbfedc78cf3df7a6e335a149a51c75b8e Mon Sep 17 00:00:00 2001 From: martha Date: Mon, 6 Jan 2025 15:52:57 -0500 Subject: [PATCH 13/13] Remove unnecessary cast --- src/components/elements/table/tableRowActionUtil.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/elements/table/tableRowActionUtil.tsx b/src/components/elements/table/tableRowActionUtil.tsx index 729aabc8c..9b8f7b5d8 100644 --- a/src/components/elements/table/tableRowActionUtil.tsx +++ b/src/components/elements/table/tableRowActionUtil.tsx @@ -43,7 +43,7 @@ export const getViewEnrollmentMenuItem = ( return { title: 'View Enrollment', key: 'enrollment', - ariaLabel: `View Enrollment, ${clientBriefName(client as ClientNameFragment)} ${entryExitRange(enrollment)}`, + ariaLabel: `View Enrollment, ${clientBriefName(client)} ${entryExitRange(enrollment)}`, to: generateSafePath(EnrollmentDashboardRoutes.ENROLLMENT_OVERVIEW, { clientId: client.id, enrollmentId: enrollment.id,