diff --git a/src/components/elements/CommonMenuButton.tsx b/src/components/elements/CommonMenuButton.tsx index 009340c67..a1bf8fc4a 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; openInNew?: boolean; }; diff --git a/src/components/elements/RelativeDateDisplay.tsx b/src/components/elements/RelativeDateDisplay.tsx index bbac95450..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 { @@ -29,6 +31,7 @@ export interface RelativeDateDisplayProps { dateString: string; prefixVerb?: string; suffixText?: string; + tooltipSuffixText?: string; TooltipProps?: Omit; TypographyProps?: TypographyProps; } @@ -40,6 +43,7 @@ const RelativeDateDisplay = ({ dateString, prefixVerb, suffixText, + tooltipSuffixText, TooltipProps = {}, TypographyProps = {}, }: RelativeDateDisplayProps) => { @@ -54,7 +58,7 @@ const RelativeDateDisplay = ({ - {formattedDate} + {formattedDate} {tooltipSuffixText} } arrow @@ -69,7 +73,12 @@ const RelativeDateDisplay = ({ ...TypographyProps.sx, }} > - {prefixVerb || null} {formattedDateRelative} {suffixText || null} + {/* Include the tooltip text as visually hidden for accessibility */} + {prefixVerb || null} {formattedDateRelative}{' '} + + ({formattedDate} {tooltipSuffixText}) + {' '} + {suffixText || null} ); diff --git a/src/components/elements/table/GenericTable.stories.tsx b/src/components/elements/table/GenericTable.stories.tsx index 6a437eba4..3af453f32 100644 --- a/src/components/elements/table/GenericTable.stories.tsx +++ b/src/components/elements/table/GenericTable.stories.tsx @@ -2,8 +2,13 @@ import { Box, Paper } from '@mui/material'; import { Meta, StoryFn } from '@storybook/react'; import GenericTable, { Props as GenericTableProps } from './GenericTable'; +import TableRowActions from '@/components/elements/table/TableRowActions'; +import { BASE_ACTION_COLUMN_DEF } from '@/components/elements/table/tableRowActionUtil'; import { SsnDobShowContextProvider } from '@/modules/client/providers/ClientSsnDobVisibility'; -import { getCustomDataElementColumns } from '@/modules/hmis/hmisUtil'; +import { + clientBriefName, + getCustomDataElementColumns, +} from '@/modules/hmis/hmisUtil'; import { CLIENT_COLUMNS } from '@/modules/search/components/ClientSearch'; import { RITA_ACKROYD } from '@/test/__mocks__/requests'; import { ClientFieldsFragment, DisplayHook } from '@/types/gqlTypes'; @@ -30,7 +35,6 @@ const Template = ); const clientColumns = [ - CLIENT_COLUMNS.id, CLIENT_COLUMNS.first, CLIENT_COLUMNS.last, CLIENT_COLUMNS.ssn, @@ -136,8 +140,37 @@ const rowsWithCdes = [ WithCustomDataElements.args = { rows: rowsWithCdes, columns: [ - CLIENT_COLUMNS.id, CLIENT_COLUMNS.name, ...getCustomDataElementColumns(rowsWithCdes), ], }; + +export const WithTableRowActions = Template().bind({}); +WithTableRowActions.args = { + rows: fakeRows, + columns: [ + CLIENT_COLUMNS.name, + { + ...BASE_ACTION_COLUMN_DEF, + render: (record) => ( + + alert(`Hello, ${clientBriefName(record)} ${record.id}`), + }} + secondaryActionConfigs={[ + { + title: 'Navigate to a link', + key: 'link', + to: 'https://storybook.js.org/docs', // just link somewhere random to show `to` prop working + }, + ]} + /> + ), + }, + ], +}; diff --git a/src/components/elements/table/GenericTable.tsx b/src/components/elements/table/GenericTable.tsx index 6d80fcb7a..93fb6d783 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, @@ -40,13 +41,11 @@ import { isRenderFunction, RenderFunction, } from './types'; -import { LocationState } from '@/routes/routeUtil'; export interface Props { rows: T[]; handleRowClick?: (row: T) => void; rowLinkTo?: (row: T) => To | null | undefined; - rowLinkState?: LocationState; columns?: ColumnDef[]; paginated?: boolean; loading?: boolean; @@ -71,7 +70,6 @@ export interface Props { noData?: ReactNode; // columnKeys contains the keys of columns currently rendered, so renderRow knows about which optional columns are shown/hidden. renderRow?: (row: T, columnKeys: string[]) => ReactNode; - condensed?: boolean; // TableBodyComponent can be overridden. This should only be used by tables that take over rendering using renderRow and render a `tbody` within their custom render fn TableBodyComponent?: ComponentType | keyof JSX.IntrinsicElements; belowRowsContent?: ReactNode; // component to insert below all rendered rows, above footer @@ -141,8 +139,6 @@ const GenericTable = ({ renderRow, noData = 'No data', loadingVariant = 'circular', - condensed = false, - rowLinkState, TableBodyComponent = TableBody, belowRowsContent, }: Props) => { @@ -247,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} + )} ))} @@ -423,7 +424,6 @@ const GenericTable = ({ {isLinked ? ( ({ height: '100%', alignItems: 'center', px: 2, - py: condensed ? 1 : 2, + py: 2, }} > {renderCellContents(row, render)} diff --git a/src/components/elements/table/TableRowActions.tsx b/src/components/elements/table/TableRowActions.tsx index 60ae05379..014a5d662 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'; @@ -23,16 +23,33 @@ const TableRowActions = ({ }: TableRowActionsProps) => { 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 }; @@ -42,3 +50,38 @@ export const getViewEnrollmentMenuItem = ( }), }; }; + +export const getViewAssessmentMenuItem = ( + assessment: AssessmentFieldsFragment, + clientId: string, + enrollmentId: string, + individualViewOnly?: boolean +) => { + return { + title: 'View Assessment', + key: 'assessment', + ariaLabel: `View Assessment, ${assessmentDescription(assessment)}`, + to: generateAssessmentPath( + assessment, + clientId, + enrollmentId, + individualViewOnly + ), + }; +}; + +export const getViewServiceMenuItem = ( + service: Pick, + enrollmentId: string, + clientId: string +) => { + return { + title: 'View Service', + key: 'service', + ariaLabel: `View Service, ${getServiceTypeForDisplay(service.serviceType)} on ${parseAndFormatDate(service.dateProvided)}`, + to: generateSafePath(EnrollmentDashboardRoutes.SERVICES, { + clientId: clientId, + enrollmentId: enrollmentId, + }), + }; +}; diff --git a/src/components/elements/table/types.tsx b/src/components/elements/table/types.tsx index 272914667..d79e5c3e7 100644 --- a/src/components/elements/table/types.tsx +++ b/src/components/elements/table/types.tsx @@ -1,16 +1,12 @@ import { TableCellProps } from '@mui/material'; -import { ReactNode } from 'react'; export type AttributeName = keyof T; export type RenderFunction = (value: T) => React.ReactNode; -export interface ColumnDef { - header?: string | ReactNode; +type BaseColumnDef = { render: AttributeName | RenderFunction; width?: string; minWidth?: string; - // unique key for element. if not provided, header is used. - key?: string; // whether to hide this column hide?: boolean; // whether to show link treatment for this cell. rowLinkTo must be provided. @@ -23,7 +19,20 @@ export interface ColumnDef { tableCellProps?: TableCellProps; optional?: boolean; defaultHidden?: boolean; -} +}; + +export type ColumnDef = + | (BaseColumnDef & { + // Header is the text to display in the header cell for this column. It's optional (see below) + header: string | React.ReactNode; + // key is an optional unique key for this column. If not provided, header is used. + key?: string; + }) + | (BaseColumnDef & { + // If header is not provided, then key is required + header?: never; + key: string; + }); export function isPrimitive(value: any): value is AttributeName { return ( diff --git a/src/modules/admin/components/services/ServiceTypeTable.tsx b/src/modules/admin/components/services/ServiceTypeTable.tsx index 2e0db0ce1..cc7417968 100644 --- a/src/modules/admin/components/services/ServiceTypeTable.tsx +++ b/src/modules/admin/components/services/ServiceTypeTable.tsx @@ -1,7 +1,10 @@ import { Chip } from '@mui/material'; +import TableRowActions from '@/components/elements/table/TableRowActions'; +import { BASE_ACTION_COLUMN_DEF } from '@/components/elements/table/tableRowActionUtil'; import { ColumnDef } from '@/components/elements/table/types'; import GenericTableWithData from '@/modules/dataFetching/components/GenericTableWithData'; import { useFilters } from '@/modules/hmis/filterUtil'; +import { getServiceTypeForDisplay } from '@/modules/services/serviceColumns'; import { AdminDashboardRoutes } from '@/routes/routes'; import { GetServiceTypesDocument, @@ -11,11 +14,10 @@ import { } from '@/types/gqlTypes'; import { generateSafePath } from '@/utils/pathEncoding'; -const columns: ColumnDef[] = [ +const COLUMNS: ColumnDef[] = [ { header: 'Service Name', render: 'name', - linkTreatment: true, }, { header: 'Service Category', @@ -40,6 +42,22 @@ const columns: ColumnDef[] = [ ) : null, }, + { + ...BASE_ACTION_COLUMN_DEF, + render: (row: ServiceTypeConfigFieldsFragment) => ( + + ), + }, ]; const ServiceTypeTable = () => { @@ -57,17 +75,12 @@ const ServiceTypeTable = () => { > queryVariables={{}} queryDocument={GetServiceTypesDocument} - columns={columns} + columns={COLUMNS} pagePath='serviceTypes' noData='No service types' filters={filters} recordType='ServiceType' paginationItemName='service type' - rowLinkTo={(row) => - generateSafePath(AdminDashboardRoutes.CONFIGURE_SERVICE_TYPE, { - serviceTypeId: row.id, - }) - } /> ); diff --git a/src/modules/admin/components/users/AdminUsers.tsx b/src/modules/admin/components/users/AdminUsers.tsx index 8404aef94..9214453c7 100644 --- a/src/modules/admin/components/users/AdminUsers.tsx +++ b/src/modules/admin/components/users/AdminUsers.tsx @@ -39,6 +39,7 @@ const AdminUsers = () => { render: ({ email }) => email, }, { + key: 'Actions', textAlign: 'right', render: (user) => access && ( diff --git a/src/modules/assessments/components/pages/ClientAssessmentsPage.tsx b/src/modules/assessments/components/pages/ClientAssessmentsPage.tsx index 95dd8031d..bd3497cb7 100644 --- a/src/modules/assessments/components/pages/ClientAssessmentsPage.tsx +++ b/src/modules/assessments/components/pages/ClientAssessmentsPage.tsx @@ -1,17 +1,17 @@ import { Paper } from '@mui/material'; -import { useCallback } from 'react'; +import { useMemo } from 'react'; +import TableRowActions from '@/components/elements/table/TableRowActions'; +import { + BASE_ACTION_COLUMN_DEF, + getViewAssessmentMenuItem, +} from '@/components/elements/table/tableRowActionUtil'; import { ColumnDef } from '@/components/elements/table/types'; import PageTitle from '@/components/layout/PageTitle'; import useSafeParams from '@/hooks/useSafeParams'; import { ClientAssessmentType } from '@/modules/assessments/assessmentTypes'; -import { - ASSESSMENT_COLUMNS, - ASSESSMENT_ENROLLMENT_COLUMNS, - assessmentRowLinkTo, -} from '@/modules/assessments/util'; +import { ASSESSMENT_COLUMNS } from '@/modules/assessments/util'; import GenericTableWithData from '@/modules/dataFetching/components/GenericTableWithData'; -import AssessmentDateWithStatusIndicator from '@/modules/hmis/components/AssessmentDateWithStatusIndicator'; import { useFilters } from '@/modules/hmis/filterUtil'; import { assessmentDescription } from '@/modules/hmis/hmisUtil'; import { @@ -21,32 +21,41 @@ import { GetClientAssessmentsQueryVariables, } from '@/types/gqlTypes'; -const columns: ColumnDef[] = [ - ASSESSMENT_COLUMNS.linkedType, - { - header: 'Assessment Date', - render: (a) => , - ariaLabel: (row) => assessmentDescription(row), - }, - { - header: 'Project Name', - render: (row) => row.enrollment.projectName, - }, - ASSESSMENT_ENROLLMENT_COLUMNS.period, -]; - const ClientAssessmentsPage = () => { const { clientId } = useSafeParams() as { clientId: string }; - const rowLinkTo = useCallback( - (record: ClientAssessmentType) => assessmentRowLinkTo(record, clientId), - [clientId] - ); - const filters = useFilters({ type: 'AssessmentFilterOptions', }); + const columns: ColumnDef[] = useMemo( + () => [ + ASSESSMENT_COLUMNS.date, + { + header: 'Project Name', + render: (row: ClientAssessmentType) => row.enrollment.projectName, + }, + ASSESSMENT_COLUMNS.type, + ASSESSMENT_COLUMNS.lastUpdated, + { + ...BASE_ACTION_COLUMN_DEF, + render: (row: ClientAssessmentType) => ( + + ), + }, + ], + [clientId] + ); + return ( <> @@ -59,7 +68,6 @@ const ClientAssessmentsPage = () => { filters={filters} queryVariables={{ id: clientId }} queryDocument={GetClientAssessmentsDocument} - rowLinkTo={rowLinkTo} columns={columns} pagePath='client.assessments' fetchPolicy='cache-and-network' diff --git a/src/modules/assessments/util.tsx b/src/modules/assessments/util.tsx index 885eccdc5..fd58f31c1 100644 --- a/src/modules/assessments/util.tsx +++ b/src/modules/assessments/util.tsx @@ -1,10 +1,10 @@ import { AlwaysPresentLocalConstants } from '../form/util/formUtil'; import { ClientAssessmentType } from './assessmentTypes'; +import RelativeDateDisplay from '@/components/elements/RelativeDateDisplay'; import { ColumnDef } from '@/components/elements/table/types'; import { HhmAssessmentType } from '@/modules/enrollment/components/HouseholdAssessmentsTable'; import AssessmentDateWithStatusIndicator from '@/modules/hmis/components/AssessmentDateWithStatusIndicator'; -import EnrollmentDateRangeWithStatus from '@/modules/hmis/components/EnrollmentDateRangeWithStatus'; -import { formRoleDisplay, lastUpdatedBy } from '@/modules/hmis/hmisUtil'; +import { clientBriefName, formRoleDisplay } from '@/modules/hmis/hmisUtil'; import { ProjectAssessmentType } from '@/modules/projects/components/ProjectAssessments'; import { EnrollmentDashboardRoutes } from '@/routes/routes'; import { AssessmentFieldsFragment, AssessmentRole } from '@/types/gqlTypes'; @@ -65,18 +65,6 @@ export const generateAssessmentPath = ( }); }; -export const assessmentRowLinkTo = ( - record: ClientAssessmentType | ProjectAssessmentType, - clientId: string -) => - // Note: this opens the assessment for individual viewing, even - // if it's an intake/exit in a multimember household. - generateSafePath(EnrollmentDashboardRoutes.VIEW_ASSESSMENT, { - clientId: clientId, - enrollmentId: record.enrollment.id, - assessmentId: record.id, - }); - export const ASSESSMENT_COLUMNS: { [key: string]: ColumnDef< | ProjectAssessmentType @@ -93,21 +81,23 @@ export const ASSESSMENT_COLUMNS: { header: 'Assessment Type', render: (a) => formRoleDisplay(a), }, - linkedType: { - header: 'Assessment Type', - render: (a) => formRoleDisplay(a), - linkTreatment: true, - }, lastUpdated: { header: 'Last Updated', - render: (e) => lastUpdatedBy(e.dateUpdated, e.user), + render: ({ dateUpdated, user }) => { + if (dateUpdated) + return ( + + ); + }, }, }; -export const ASSESSMENT_ENROLLMENT_COLUMNS: { - [key: string]: ColumnDef; -} = { - period: { - header: 'Enrollment Period', - render: (a) => , - }, + +export const ASSESSMENT_CLIENT_NAME_COL: ColumnDef< + ProjectAssessmentType | HhmAssessmentType +> = { + header: 'Client Name', + render: (a) => clientBriefName(a.enrollment.client), }; diff --git a/src/modules/bulkServices/components/AssignServiceButton.tsx b/src/modules/bulkServices/components/AssignServiceButton.tsx index 4e02bb39b..5e6de0326 100644 --- a/src/modules/bulkServices/components/AssignServiceButton.tsx +++ b/src/modules/bulkServices/components/AssignServiceButton.tsx @@ -126,6 +126,7 @@ const AssignServiceButton: React.FC = ({ fullWidth variant='contained' color={isAssignedOnDate ? 'primary' : 'grayscale'} + aria-label={`${buttonText}, ${clientBriefName(client)}`} > {buttonText} diff --git a/src/modules/bulkServices/components/BulkServicesTable.tsx b/src/modules/bulkServices/components/BulkServicesTable.tsx index 715505d9e..ecbadd511 100644 --- a/src/modules/bulkServices/components/BulkServicesTable.tsx +++ b/src/modules/bulkServices/components/BulkServicesTable.tsx @@ -3,26 +3,31 @@ import { ServicePeriod } from '../bulkServicesTypes'; import AssignServiceButton from './AssignServiceButton'; import MultiAssignServiceButton from './MultiAssignServiceButton'; import NotCollectedText from '@/components/elements/NotCollectedText'; -import RouterLink from '@/components/elements/RouterLink'; +import TableRowActions from '@/components/elements/table/TableRowActions'; +import { + BASE_ACTION_COLUMN_DEF, + getViewClientMenuItem, + getViewEnrollmentMenuItem, +} from '@/components/elements/table/tableRowActionUtil'; import { ColumnDef } from '@/components/elements/table/types'; import { SsnDobShowContextProvider } from '@/modules/client/providers/ClientSsnDobVisibility'; import GenericTableWithData from '@/modules/dataFetching/components/GenericTableWithData'; import { + clientBriefName, formatDateForDisplay, formatDateForGql, formatRelativeDate, parseAndFormatDate, parseHmisDateString, } from '@/modules/hmis/hmisUtil'; +import { useHasRootPermissions } from '@/modules/permissions/useHasPermissionsHooks'; import { CLIENT_COLUMNS } from '@/modules/search/components/ClientSearch'; -import { EnrollmentDashboardRoutes } from '@/routes/routes'; import { BulkServicesClientSearchDocument, BulkServicesClientSearchQuery, BulkServicesClientSearchQueryVariables, ClientSortOption, } from '@/types/gqlTypes'; -import { generateSafePath } from '@/utils/pathEncoding'; interface Props { projectId: string; @@ -59,6 +64,8 @@ const BulkServicesTable: React.FC = ({ [cocCode, projectId, serviceDate, serviceTypeId] ); + const [canViewDob] = useHasRootPermissions(['canViewDob']); + const getColumnDefs = useCallback( (_rows: RowType[], loading?: boolean) => { const notEnrolledText = ( @@ -67,26 +74,14 @@ const BulkServicesTable: React.FC = ({ ); return [ - { ...CLIENT_COLUMNS.linkedId }, - CLIENT_COLUMNS.first, - CLIENT_COLUMNS.last, - CLIENT_COLUMNS.dobAge, + CLIENT_COLUMNS.name, + ...(canViewDob ? [CLIENT_COLUMNS.dobAge] : []), { header: 'Entry Date', render: (row: RowType) => { if (!row.activeEnrollment) return notEnrolledText; - return ( - - {parseAndFormatDate(row.activeEnrollment.entryDate)} - - ); + return parseAndFormatDate(row.activeEnrollment.entryDate); }, }, { @@ -112,30 +107,43 @@ const BulkServicesTable: React.FC = ({ }, }, { - header: `Assign ${serviceTypeName} for ${formatDateForDisplay( - serviceDate - )}`, - width: '180px', - render: (row: RowType) => { - return ( - - ); - }, + ...BASE_ACTION_COLUMN_DEF, + render: (row: RowType) => ( + + } + secondaryActionConfigs={[ + getViewClientMenuItem(row), + ...(row.activeEnrollment + ? [getViewEnrollmentMenuItem(row.activeEnrollment, row)] + : []), + ]} + /> + ), }, ] as ColumnDef[]; }, - [serviceDate, serviceTypeName, mutationQueryVariables, anyRowsSelected] + [ + anyRowsSelected, + canViewDob, + mutationQueryVariables, + serviceDate, + serviceTypeName, + ] ); const defaultFilterValues = useMemo(() => { diff --git a/src/modules/caseNotes/components/ClientCaseNotes.tsx b/src/modules/caseNotes/components/ClientCaseNotes.tsx index 096608ffe..152e91f33 100644 --- a/src/modules/caseNotes/components/ClientCaseNotes.tsx +++ b/src/modules/caseNotes/components/ClientCaseNotes.tsx @@ -1,41 +1,26 @@ import { Paper } from '@mui/material'; +import { useMemo } from 'react'; import { CASE_NOTE_COLUMNS } from './EnrollmentCaseNotes'; -import { ColumnDef } from '@/components/elements/table/types'; +import TableRowActions from '@/components/elements/table/TableRowActions'; +import { BASE_ACTION_COLUMN_DEF } from '@/components/elements/table/tableRowActionUtil'; import PageTitle from '@/components/layout/PageTitle'; import NotFound from '@/components/pages/NotFound'; import useClientDashboardContext from '@/modules/client/hooks/useClientDashboardContext'; import GenericTableWithData from '@/modules/dataFetching/components/GenericTableWithData'; import { useViewEditRecordDialogs } from '@/modules/form/hooks/useViewEditRecordDialogs'; -import EnrollmentDateRangeWithStatus from '@/modules/hmis/components/EnrollmentDateRangeWithStatus'; +import { parseAndFormatDate } from '@/modules/hmis/hmisUtil'; import { - RecordFormRole, GetClientCaseNotesDocument, GetClientCaseNotesQuery, GetClientCaseNotesQueryVariables, + RecordFormRole, } from '@/types/gqlTypes'; type Row = NonNullable< GetClientCaseNotesQuery['client'] >['customCaseNotes']['nodes'][0]; -const columns: ColumnDef[] = [ - CASE_NOTE_COLUMNS.InformationDate, - CASE_NOTE_COLUMNS.NoteContentPreview, - { - key: 'project', - header: 'Project Name', - render: (row) => row.enrollment.projectName, - }, - { - key: 'en-period', - header: 'Enrollment Period', - render: (row) => ( - - ), - }, -]; - const ClientCaseNotes = () => { const { client } = useClientDashboardContext(); @@ -49,6 +34,34 @@ const ClientCaseNotes = () => { maxWidth: 'sm', }); + const columns = useMemo( + () => [ + CASE_NOTE_COLUMNS.InformationDate, + { + key: 'project', + header: 'Project Name', + render: (row: Row) => row.enrollment.projectName, + }, + CASE_NOTE_COLUMNS.LastUpdated, + CASE_NOTE_COLUMNS.NoteContentPreview, + { + ...BASE_ACTION_COLUMN_DEF, + render: (row: Row) => ( + onSelectRecord(row), + }} + /> + ), + }, + ], + [onSelectRecord] + ); + if (!clientId) return ; return ( @@ -66,7 +79,6 @@ const ClientCaseNotes = () => { pagePath='client.customCaseNotes' noData='No case notes' headerCellSx={() => ({ color: 'text.secondary' })} - handleRowClick={onSelectRecord} recordType='CustomCaseNote' paginationItemName='case note' showTopToolbar diff --git a/src/modules/caseNotes/components/EnrollmentCaseNotes.tsx b/src/modules/caseNotes/components/EnrollmentCaseNotes.tsx index 3aff105ed..76225b3b4 100644 --- a/src/modules/caseNotes/components/EnrollmentCaseNotes.tsx +++ b/src/modules/caseNotes/components/EnrollmentCaseNotes.tsx @@ -4,13 +4,15 @@ import { Box, Button } from '@mui/material'; import { useCallback } from 'react'; import { useViewEditRecordDialogs } from '../../form/hooks/useViewEditRecordDialogs'; +import RelativeDateDisplay from '@/components/elements/RelativeDateDisplay'; +import TableRowActions from '@/components/elements/table/TableRowActions'; +import { BASE_ACTION_COLUMN_DEF } from '@/components/elements/table/tableRowActionUtil'; import TitleCard from '@/components/elements/TitleCard'; import NotFound from '@/components/pages/NotFound'; import GenericTableWithData from '@/modules/dataFetching/components/GenericTableWithData'; import useEnrollmentDashboardContext from '@/modules/enrollment/hooks/useEnrollmentDashboardContext'; import { getCustomDataElementColumns, - lastUpdatedBy, parseAndFormatDate, } from '@/modules/hmis/hmisUtil'; import { cache } from '@/providers/apolloClient'; @@ -30,10 +32,9 @@ export const CASE_NOTE_COLUMNS = { width: '150px', render: ({ informationDate }: CustomCaseNoteFieldsFragment) => parseAndFormatDate(informationDate), - linkTreatment: true, }, NoteContent: { - header: 'Note', + header: 'Note Content', render: ({ content }: CustomCaseNoteFieldsFragment) => ( ( - lastUpdatedBy(dateUpdated, user), + render: ({ dateUpdated, user }: CustomCaseNoteFieldsFragment) => { + if (dateUpdated) + return ( + + ); + }, }, }; @@ -102,15 +110,34 @@ const EnrollmentCaseNotes = () => { projectId: enrollment?.project.id, }); - const getColumnDefs = useCallback((rows: CustomCaseNoteFieldsFragment[]) => { - const customColumns = getCustomDataElementColumns(rows); - return [ - CASE_NOTE_COLUMNS.InformationDate, - ...customColumns, - CASE_NOTE_COLUMNS.NoteContent, - CASE_NOTE_COLUMNS.LastUpdated, - ]; - }, []); + const getColumnDefs = useCallback( + (rows: CustomCaseNoteFieldsFragment[]) => { + const customColumns = getCustomDataElementColumns(rows); + return [ + CASE_NOTE_COLUMNS.InformationDate, + ...customColumns, + CASE_NOTE_COLUMNS.NoteContent, + CASE_NOTE_COLUMNS.LastUpdated, + { + ...BASE_ACTION_COLUMN_DEF, + render: (caseNote: CustomCaseNoteFieldsFragment) => ( + onSelectRecord(caseNote), + }} + /> + ), + }, + ]; + }, + [onSelectRecord] + ); const caseNotesFeature = getEnrollmentFeature( DataCollectionFeatureRole.CaseNote @@ -149,7 +176,6 @@ const EnrollmentCaseNotes = () => { pagePath='enrollment.customCaseNotes' noData='No case notes' headerCellSx={() => ({ color: 'text.secondary' })} - handleRowClick={onSelectRecord} recordType='CustomCaseNote' paginationItemName='case note' showTopToolbar diff --git a/src/modules/client/components/ClientEnrollmentCard.tsx b/src/modules/client/components/ClientEnrollmentCard.tsx index f2392de7d..fea818e28 100644 --- a/src/modules/client/components/ClientEnrollmentCard.tsx +++ b/src/modules/client/components/ClientEnrollmentCard.tsx @@ -73,7 +73,6 @@ const RecentEnrollments = ({ { key: 'name', header: 'Name', - linkTreatment: true, render: 'projectName', }, { diff --git a/src/modules/client/components/ClientSearchResultCard.tsx b/src/modules/client/components/ClientSearchResultCard.tsx index ca7249901..d51900093 100644 --- a/src/modules/client/components/ClientSearchResultCard.tsx +++ b/src/modules/client/components/ClientSearchResultCard.tsx @@ -219,7 +219,7 @@ const ClientSearchResultCard: React.FC = ({ Icon={PersonIcon} leftAlign > - Client Profile + View Client {/* disabled for now #185750557 */} {/* diff --git a/src/modules/clientFiles/components/ClientFilesPage.tsx b/src/modules/clientFiles/components/ClientFilesPage.tsx index 9d9fb9459..8175e541e 100644 --- a/src/modules/clientFiles/components/ClientFilesPage.tsx +++ b/src/modules/clientFiles/components/ClientFilesPage.tsx @@ -1,18 +1,19 @@ import UploadIcon from '@mui/icons-material/Upload'; -import { Box, Chip, Link, Paper, Stack, Typography } from '@mui/material'; +import { Box, Chip, Paper, Typography } from '@mui/material'; import { useMemo, useState } from 'react'; import useFileActions from '../hooks/useFileActions'; import ButtonLink from '@/components/elements/ButtonLink'; import NotCollectedText from '@/components/elements/NotCollectedText'; +import RelativeDateDisplay from '@/components/elements/RelativeDateDisplay'; +import TableRowActions from '@/components/elements/table/TableRowActions'; +import { BASE_ACTION_COLUMN_DEF } from '@/components/elements/table/tableRowActionUtil'; import { ColumnDef } from '@/components/elements/table/types'; import FilePreviewDialog from '@/components/elements/upload/fileDialog/FilePreviewDialog'; import PageTitle from '@/components/layout/PageTitle'; import useSafeParams from '@/hooks/useSafeParams'; import GenericTableWithData from '@/modules/dataFetching/components/GenericTableWithData'; -import EnrollmentDateRangeWithStatus from '@/modules/hmis/components/EnrollmentDateRangeWithStatus'; -import { parseAndFormatDateTime } from '@/modules/hmis/hmisUtil'; import { useClientPermissions, useHasClientPermissions, @@ -75,19 +76,9 @@ const ClientFilesPage = () => { return [ { header: 'File Name', - render: (file) => - file.redacted ? ( - {file.name} - ) : ( - setViewingFile(file)} - align='left' - tabIndex={-1} - > - {file.name} - - ), + render: (file) => ( + {file.name} + ), }, { header: 'File Tags', @@ -111,30 +102,48 @@ const ClientFilesPage = () => { ) : null, }, { - header: 'Enrollment', - render: ({ enrollment }) => { - if (!enrollment) return N/A; - - return ( - - {enrollment.projectName} - - - ); - }, + header: 'Project Name', + render: ({ enrollment }) => + enrollment ? ( + enrollment.projectName + ) : ( + N/A + ), }, { - header: 'Uploaded At', - render: (file) => { - const uploadedAt = file.dateCreated - ? parseAndFormatDateTime(file.dateCreated) - : 'Unknown time'; - const uploadedBy = file.uploadedBy?.name - ? `by ${file.uploadedBy?.name}` + header: 'Uploaded', + render: ({ dateCreated, uploadedBy }) => { + const byUser = uploadedBy?.name + ? `by ${uploadedBy?.name}` : 'by unknown user'; - return `${uploadedAt} ${uploadedBy}`; + if (dateCreated) + return ( + + ); + return `Unknown time ${byUser}`; }, }, + { + ...BASE_ACTION_COLUMN_DEF, + render: (file) => ( + setViewingFile(file), + } + } + /> + ), + }, ]; }, [pickListData]); @@ -166,7 +175,6 @@ const ClientFilesPage = () => { queryDocument={GetClientFilesDocument} columns={columns} pagePath='client.files' - handleRowClick={(file) => !file.redacted && setViewingFile(file)} noData='No files' /> diff --git a/src/modules/enrollment/components/EnrollmentAssessmentActionButtons.tsx b/src/modules/enrollment/components/EnrollmentAssessmentActionButtons.tsx index ac4b5c6dc..717e3a036 100644 --- a/src/modules/enrollment/components/EnrollmentAssessmentActionButtons.tsx +++ b/src/modules/enrollment/components/EnrollmentAssessmentActionButtons.tsx @@ -88,6 +88,8 @@ const NewAssessmentMenu: React.FC< ); if (items.length === 0) { + // Assessment eligibilities will have length 0 for an exited enrollment where no post-exit assessments are enabled. + // (Custom Assessments can collected be post-exit) return ( [] = [ - ASSESSMENT_COLUMNS.linkedType, - ASSESSMENT_COLUMNS.date, - ASSESSMENT_COLUMNS.lastUpdated, -]; - interface Props { enrollmentId: string; clientId: string; @@ -31,9 +27,26 @@ const EnrollmentAssessmentsTable: React.FC = ({ enrollmentId, projectId, }) => { - const rowLinkTo = useCallback( - (assessment: AssessmentFieldsFragment) => - generateAssessmentPath(assessment, clientId, enrollmentId), + const columns = useMemo( + () => [ + ASSESSMENT_COLUMNS.date, + ASSESSMENT_COLUMNS.type, + ASSESSMENT_COLUMNS.lastUpdated, + { + ...BASE_ACTION_COLUMN_DEF, + render: (assessment) => ( + + ), + }, + ], [clientId, enrollmentId] ); @@ -51,7 +64,6 @@ const EnrollmentAssessmentsTable: React.FC = ({ filters={filters} queryVariables={{ id: enrollmentId }} queryDocument={GetEnrollmentAssessmentsDocument} - rowLinkTo={rowLinkTo} columns={columns} pagePath='enrollment.assessments' noData='No assessments' diff --git a/src/modules/enrollment/components/HouseholdAssessmentsTable.tsx b/src/modules/enrollment/components/HouseholdAssessmentsTable.tsx index deeb6f388..2dbd7018a 100644 --- a/src/modules/enrollment/components/HouseholdAssessmentsTable.tsx +++ b/src/modules/enrollment/components/HouseholdAssessmentsTable.tsx @@ -1,13 +1,19 @@ -import { useCallback } from 'react'; - +import TableRowActions from '@/components/elements/table/TableRowActions'; +import { + BASE_ACTION_COLUMN_DEF, + getViewAssessmentMenuItem, +} from '@/components/elements/table/tableRowActionUtil'; import { ColumnDef } from '@/components/elements/table/types'; import { + ASSESSMENT_CLIENT_NAME_COL, ASSESSMENT_COLUMNS, - generateAssessmentPath, } from '@/modules/assessments/util'; import GenericTableWithData from '@/modules/dataFetching/components/GenericTableWithData'; import { useFilters } from '@/modules/hmis/filterUtil'; -import { clientBriefName } from '@/modules/hmis/hmisUtil'; +import { + assessmentDescription, + clientBriefName, +} from '@/modules/hmis/hmisUtil'; import { GetHouseholdAssessmentsDocument, GetHouseholdAssessmentsQuery, @@ -18,14 +24,29 @@ export type HhmAssessmentType = NonNullable< NonNullable['assessments'] >['nodes'][0]; -const columns: ColumnDef[] = [ - { - header: 'Client Name', - render: (a) => clientBriefName(a.enrollment.client), - }, - ASSESSMENT_COLUMNS.linkedType, +const COLUMNS: ColumnDef[] = [ + ASSESSMENT_CLIENT_NAME_COL, ASSESSMENT_COLUMNS.date, + ASSESSMENT_COLUMNS.type, ASSESSMENT_COLUMNS.lastUpdated, + { + ...BASE_ACTION_COLUMN_DEF, + render: (assessment: HhmAssessmentType) => ( + + ), + }, ]; interface Props { @@ -37,16 +58,6 @@ const HouseholdAssessmentsTable: React.FC = ({ householdId, projectId, }) => { - const rowLinkTo = useCallback( - (assessment: HhmAssessmentType) => - generateAssessmentPath( - assessment, - assessment.enrollment.client.id, - assessment.enrollment.id - ), - [] - ); - const filters = useFilters({ type: 'AssessmentsForHouseholdFilterOptions', pickListArgs: { projectId }, @@ -61,8 +72,7 @@ const HouseholdAssessmentsTable: React.FC = ({ filters={filters} queryVariables={{ id: householdId }} queryDocument={GetHouseholdAssessmentsDocument} - rowLinkTo={rowLinkTo} - columns={columns} + columns={COLUMNS} pagePath='household.assessments' noData='No assessments' recordType='Assessment' diff --git a/src/modules/enrollment/components/pages/ClientEnrollmentsPage.tsx b/src/modules/enrollment/components/pages/ClientEnrollmentsPage.tsx index 8fc82c2e5..1e63f7698 100644 --- a/src/modules/enrollment/components/pages/ClientEnrollmentsPage.tsx +++ b/src/modules/enrollment/components/pages/ClientEnrollmentsPage.tsx @@ -1,19 +1,24 @@ import { Paper, Stack, Typography } from '@mui/material'; -import { ReactNode, useCallback } from 'react'; +import { ReactNode, useMemo } from 'react'; import NotCollectedText from '@/components/elements/NotCollectedText'; +import TableRowActions from '@/components/elements/table/TableRowActions'; +import { + BASE_ACTION_COLUMN_DEF, + getViewEnrollmentMenuItem, +} from '@/components/elements/table/tableRowActionUtil'; import { ColumnDef } from '@/components/elements/table/types'; import PageTitle from '@/components/layout/PageTitle'; import useClientDashboardContext from '@/modules/client/hooks/useClientDashboardContext'; import GenericTableWithData from '@/modules/dataFetching/components/GenericTableWithData'; -import EnrollmentDateRangeWithStatus from '@/modules/hmis/components/EnrollmentDateRangeWithStatus'; import ProjectTypeChip from '@/modules/hmis/components/ProjectTypeChip'; import { useFilters } from '@/modules/hmis/filterUtil'; import { - PERMANENT_HOUSING_PROJECT_TYPES, + entryExitRange, parseAndFormatDate, + PERMANENT_HOUSING_PROJECT_TYPES, } from '@/modules/hmis/hmisUtil'; -import { EnrollmentDashboardRoutes } from '@/routes/routes'; +import { ENROLLMENT_COLUMNS } from '@/modules/projects/components/tables/ProjectClientEnrollmentsTable'; import { ClientEnrollmentFieldsFragment, EnrollmentSortOption, @@ -23,7 +28,6 @@ import { ProjectType, RelationshipToHoH, } from '@/types/gqlTypes'; -import { generateSafePath } from '@/utils/pathEncoding'; const CaptionedText: React.FC<{ caption: string; children: ReactNode }> = ({ caption, @@ -39,30 +43,25 @@ const CaptionedText: React.FC<{ caption: string; children: ReactNode }> = ({ ); }; -const columns: ColumnDef[] = [ - { - header: 'Enrollment Period', - render: (row) => , +const CLIENT_ENROLLMENT_COLUMNS: { + [key: string]: ColumnDef; +} = { + projectName: { + header: 'Project Name', + render: 'projectName', }, - { + organizationName: { header: 'Organization Name', render: 'organizationName', }, - { - header: 'Project Name', - render: 'projectName', - linkTreatment: true, - ariaLabel: (row) => row.projectName, - }, - - { + projectType: { header: 'Project Type', render: ({ projectType }) => ( ), }, - { - header: 'Details', + enrollmentDetails: { + header: 'Enrollment Details', render: ({ moveInDate, lastBedNightDate, @@ -96,45 +95,45 @@ const columns: ColumnDef[] = [ } }, }, -]; +}; const ClientEnrollmentsPage = () => { const { client } = useClientDashboardContext(); - const rowLinkTo = useCallback( - (enrollment: ClientEnrollmentFieldsFragment) => { - if (!enrollment.access.canViewEnrollmentDetails) return null; - - return generateSafePath(EnrollmentDashboardRoutes.ENROLLMENT_OVERVIEW, { - clientId: client.id, - enrollmentId: enrollment.id, - }); - }, - [client] - ); - const filters = useFilters({ type: 'EnrollmentsForClientFilterOptions', }); + const columns = useMemo( + () => [ + CLIENT_ENROLLMENT_COLUMNS.projectName, + CLIENT_ENROLLMENT_COLUMNS.organizationName, + ENROLLMENT_COLUMNS.entryDate, + ENROLLMENT_COLUMNS.exitDate, + ENROLLMENT_COLUMNS.enrollmentStatus, + CLIENT_ENROLLMENT_COLUMNS.projectType, + CLIENT_ENROLLMENT_COLUMNS.enrollmentDetails, + { + ...BASE_ACTION_COLUMN_DEF, + render: (enrollment) => ( + + ), + }, + ], + [client] + ); + return ( <> - - // - // Add Enrollment - // - // - // } - /> + { > queryVariables={{ id: client.id }} queryDocument={GetClientEnrollmentsDocument} - rowLinkTo={rowLinkTo} columns={columns} pagePath='client.enrollments' filters={filters} diff --git a/src/modules/enrollment/components/pages/EnrollmentCeAssessmentsPage.tsx b/src/modules/enrollment/components/pages/EnrollmentCeAssessmentsPage.tsx index 8961cd041..680102862 100644 --- a/src/modules/enrollment/components/pages/EnrollmentCeAssessmentsPage.tsx +++ b/src/modules/enrollment/components/pages/EnrollmentCeAssessmentsPage.tsx @@ -2,6 +2,8 @@ import AddIcon from '@mui/icons-material/Add'; import { Button } from '@mui/material'; import { useCallback, useMemo } from 'react'; +import TableRowActions from '@/components/elements/table/TableRowActions'; +import { BASE_ACTION_COLUMN_DEF } from '@/components/elements/table/tableRowActionUtil'; import { ColumnDef } from '@/components/elements/table/types'; import TitleCard from '@/components/elements/TitleCard'; import NotFound from '@/components/pages/NotFound'; @@ -59,16 +61,20 @@ const EnrollmentCeAssessmentsPage = () => { projectId: enrollment?.project.id, }); + const ceAssessmentFeature = getEnrollmentFeature( + DataCollectionFeatureRole.CeAssessment + ); + const columns: ColumnDef[] = useMemo( () => [ { header: 'Assessment Date', - render: (a) => parseAndFormatDate(a.assessmentDate), - linkTreatment: canEditCeAssessments, + render: (a: CeAssessmentFieldsFragment) => + parseAndFormatDate(a.assessmentDate), }, { - header: 'Level', - render: (a) => ( + header: 'Assessment Level', + render: (a: CeAssessmentFieldsFragment) => ( { ), }, { - header: 'Type', - render: (a) => ( + header: 'Assessment Type', + render: (a: CeAssessmentFieldsFragment) => ( { ), }, { - header: 'Location', - render: (a) => a.assessmentLocation, + header: 'Assessment Location', + render: (a: CeAssessmentFieldsFragment) => a.assessmentLocation, }, { header: 'Prioritization Status', - render: (a) => ( + render: (a: CeAssessmentFieldsFragment) => ( ), }, + ...(canEditCeAssessments + ? [ + { + ...BASE_ACTION_COLUMN_DEF, + render: (a: CeAssessmentFieldsFragment) => ( + onSelectRecord(a), + }} + /> + ), + }, + ] + : []), ], - [canEditCeAssessments] - ); - - const ceAssessmentFeature = getEnrollmentFeature( - DataCollectionFeatureRole.CeAssessment + [canEditCeAssessments, onSelectRecord] ); if (!enrollment || !enrollmentId || !clientId || !ceAssessmentFeature) @@ -138,8 +160,6 @@ const EnrollmentCeAssessmentsPage = () => { pagePath='enrollment.ceAssessments' noData='No Coordinated Entry Assessments' headerCellSx={() => ({ color: 'text.secondary' })} - // no need for read-only users to click in, because they can see all the information in the table. - handleRowClick={canEditCeAssessments ? onSelectRecord : undefined} /> {editRecordDialog()} diff --git a/src/modules/enrollment/components/pages/EnrollmentCeEventsPage.tsx b/src/modules/enrollment/components/pages/EnrollmentCeEventsPage.tsx index b35d47a40..606c424ab 100644 --- a/src/modules/enrollment/components/pages/EnrollmentCeEventsPage.tsx +++ b/src/modules/enrollment/components/pages/EnrollmentCeEventsPage.tsx @@ -2,7 +2,8 @@ import AddIcon from '@mui/icons-material/Add'; import { Button } from '@mui/material'; import { useCallback, useMemo } from 'react'; -import { ColumnDef } from '@/components/elements/table/types'; +import TableRowActions from '@/components/elements/table/TableRowActions'; +import { BASE_ACTION_COLUMN_DEF } from '@/components/elements/table/tableRowActionUtil'; import TitleCard from '@/components/elements/TitleCard'; import NotFound from '@/components/pages/NotFound'; import GenericTableWithData from '@/modules/dataFetching/components/GenericTableWithData'; @@ -25,22 +26,6 @@ import { RecordFormRole, } from '@/types/gqlTypes'; -const columns: ColumnDef[] = [ - { - header: 'Event Date', - render: (e) => parseAndFormatDate(e.eventDate), - linkTreatment: true, - }, - { - header: 'Event Type', - render: (e) => , - }, - { - header: 'Result', - render: (e) => eventReferralResult(e), - }, -]; - const EnrollmentCeEventsPage = () => { const { enrollment, getEnrollmentFeature } = useEnrollmentDashboardContext(); const enrollmentId = enrollment?.id; @@ -81,6 +66,40 @@ const EnrollmentCeEventsPage = () => { DataCollectionFeatureRole.CeEvent ); + const columns = useMemo( + () => [ + { + header: 'Event Type', + render: (e: EventFieldsFragment) => ( + + ), + }, + { + header: 'Event Date', + render: (e: EventFieldsFragment) => parseAndFormatDate(e.eventDate), + }, + { + header: 'Referral Result', + render: (e: EventFieldsFragment) => eventReferralResult(e), + }, + { + ...BASE_ACTION_COLUMN_DEF, + render: (e: EventFieldsFragment) => ( + onSelectRecord(e), + }} + /> + ), + }, + ], + [onSelectRecord] + ); + if (!enrollment || !enrollmentId || !clientId || !ceEventFeature) return ; @@ -114,7 +133,6 @@ const EnrollmentCeEventsPage = () => { pagePath='enrollment.events' noData='No events' headerCellSx={() => ({ color: 'text.secondary' })} - handleRowClick={onSelectRecord} /> {viewRecordDialog()} diff --git a/src/modules/enrollment/components/pages/EnrollmentCurrentLivingSituationsPage.tsx b/src/modules/enrollment/components/pages/EnrollmentCurrentLivingSituationsPage.tsx index 4ac0dcf11..7812d2f05 100644 --- a/src/modules/enrollment/components/pages/EnrollmentCurrentLivingSituationsPage.tsx +++ b/src/modules/enrollment/components/pages/EnrollmentCurrentLivingSituationsPage.tsx @@ -1,6 +1,8 @@ import AddIcon from '@mui/icons-material/Add'; import { Button } from '@mui/material'; import { useCallback, useMemo } from 'react'; +import TableRowActions from '@/components/elements/table/TableRowActions'; +import { BASE_ACTION_COLUMN_DEF } from '@/components/elements/table/tableRowActionUtil'; import TitleCard from '@/components/elements/TitleCard'; import NotFound from '@/components/pages/NotFound'; import GenericTableWithData from '@/modules/dataFetching/components/GenericTableWithData'; @@ -23,13 +25,12 @@ import { RecordFormRole, } from '@/types/gqlTypes'; -export const baseColumns = { +export const CLS_COLUMNS = { informationDate: { header: 'Information Date', width: '180px', render: (e: CurrentLivingSituationFieldsFragment) => parseAndFormatDate(e.informationDate), - linkTreatment: true, }, livingSituation: { header: 'Living Situation', @@ -95,13 +96,30 @@ const EnrollmentCurrentLivingSituationsPage = () => { (rows: CurrentLivingSituationFieldsFragment[]) => { const customColumns = getCustomDataElementColumns(rows); return [ - baseColumns.informationDate, - baseColumns.livingSituation, - baseColumns.locationDetails, + CLS_COLUMNS.informationDate, + CLS_COLUMNS.livingSituation, + CLS_COLUMNS.locationDetails, ...customColumns, + { + ...BASE_ACTION_COLUMN_DEF, + render: (cls: CurrentLivingSituationFieldsFragment) => ( + onSelectRecord(cls), + }} + /> + ), + }, ]; }, - [] + [onSelectRecord] ); if (!enrollment || !enrollmentId || !clientId || !clsFeature) @@ -137,7 +155,6 @@ const EnrollmentCurrentLivingSituationsPage = () => { noData='No current living situations' recordType='CurrentLivingSituation' headerCellSx={() => ({ color: 'text.secondary' })} - handleRowClick={onSelectRecord} /> {viewRecordDialog()} diff --git a/src/modules/hmis/components/EnrollmentStatus.tsx b/src/modules/hmis/components/EnrollmentStatus.tsx index 38cf0d679..472a9c1a8 100644 --- a/src/modules/hmis/components/EnrollmentStatus.tsx +++ b/src/modules/hmis/components/EnrollmentStatus.tsx @@ -3,7 +3,11 @@ import HistoryIcon from '@mui/icons-material/History'; import TimerIcon from '@mui/icons-material/Timer'; import { Stack, Typography } from '@mui/material'; import { useMemo } from 'react'; -import { Enrollment, Household } from '@/types/gqlTypes'; +import { + Enrollment, + EnrollmentFieldsFragment, + Household, +} from '@/types/gqlTypes'; interface CommonStatusProps { variant: 'inProgress' | 'open' | 'autoExited' | 'exited'; @@ -57,7 +61,8 @@ const CommonStatus: React.FC = ({ variant }) => { const EnrollmentStatus = ({ enrollment, }: { - enrollment: Pick; + enrollment: Pick & + Partial>; }) => { if (enrollment.inProgress) return ; if (enrollment.autoExited) return ; diff --git a/src/modules/hmis/hmisUtil.ts b/src/modules/hmis/hmisUtil.ts index 3698f8d8b..71eb80f3f 100644 --- a/src/modules/hmis/hmisUtil.ts +++ b/src/modules/hmis/hmisUtil.ts @@ -375,12 +375,18 @@ export const formRoleDisplay = (assessment: AssessmentFieldsFragment) => { return defaultTitle; }; -export const assessmentDescription = (assessment: ClientAssessmentType) => { +export const assessmentDescription = ( + assessment: ClientAssessmentType | AssessmentFieldsFragment +) => { const prefix = formRoleDisplay(assessment); - const name = prefix ? `${prefix} assessment` : 'Assessment'; - return `${name} at ${assessment.enrollment.projectName} on ${ - parseAndFormatDate(assessment.assessmentDate) || 'unknown date' - }`; + const name = prefix ? `${prefix} assessment ` : 'Assessment '; + // `enrollment` may not be present in the type (eg. if we are on the Enrollment Assessments page) + const atProject = + 'enrollment' in assessment && !!assessment.enrollment?.projectName + ? `at ${assessment.enrollment.projectName} ` + : ''; + const onDate = `on ${parseAndFormatDate(assessment.assessmentDate) || 'unknown date'}`; + return name + atProject + onDate; }; export const eventReferralResult = (e: EventFieldsFragment) => { 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..8d8e70582 100644 --- a/src/modules/projectAdministration/components/AllProjectsPage.tsx +++ b/src/modules/projectAdministration/components/AllProjectsPage.tsx @@ -1,12 +1,14 @@ import AddIcon from '@mui/icons-material/Add'; -import { Grid, Paper } from '@mui/material'; +import { Paper } from '@mui/material'; import { Box, Stack } from '@mui/system'; -import { useCallback, useState } from 'react'; +import { Dispatch, SetStateAction, useState } from 'react'; import CommonSearchInput from '../../search/components/CommonSearchInput'; import ButtonLink from '@/components/elements/ButtonLink'; import CommonToggle, { ToggleItem } from '@/components/elements/CommonToggle'; +import TableRowActions from '@/components/elements/table/TableRowActions'; +import { BASE_ACTION_COLUMN_DEF } from '@/components/elements/table/tableRowActionUtil'; import { ColumnDef } from '@/components/elements/table/types'; import PageContainer from '@/components/layout/PageContainer'; import useDebouncedState from '@/hooks/useDebouncedState'; @@ -38,11 +40,9 @@ const PROJECT_COLUMNS: ColumnDef[] = [ { header: 'Project Name', render: 'projectName', - linkTreatment: true, - ariaLabel: (row) => row.projectName, }, { - header: 'Organization', + header: 'Organization Name', render: (row) => row.organization.organizationName, }, { @@ -59,18 +59,49 @@ const PROJECT_COLUMNS: ColumnDef[] = [ project.operatingEndDate ), }, + { + ...BASE_ACTION_COLUMN_DEF, + render: (project: ProjectAllFieldsFragment) => ( + + ), + }, ]; const ORGANIZATION_COLUMNS: ColumnDef[] = [ { header: 'Organization Name', render: 'organizationName', - linkTreatment: true, }, { header: 'Project Count', render: 'projects.nodesCount' as keyof OrganizationType, }, + { + ...BASE_ACTION_COLUMN_DEF, + render: (organization: OrganizationType) => ( + + ), + }, ]; export type ViewMode = 'projects' | 'organizations'; @@ -87,34 +118,131 @@ const toggleItemDefinitions: ToggleItem[] = [ }, ]; -const AllProjectsPage = () => { - const [viewMode, setViewMode] = useState('projects'); - - const [search, setSearch, debouncedSearch] = useDebouncedState(''); - - const projectRowLink = useCallback( - (project: ProjectAllFieldsFragment) => - generateSafePath(Routes.PROJECT, { - projectId: project.id, - }), - [] - ); - +const ProjectsTable = ({ + search, + setSearch, + debouncedSearch, +}: { + search: string; + setSearch: Dispatch>; + debouncedSearch?: string; +}) => { const projectFilters = useFilters({ type: 'ProjectFilterOptions', omit: ['searchTerm'], }); - const organizationRowLink = useCallback( - (row: OrganizationType) => - generateSafePath(Routes.ORGANIZATION, { - organizationId: row.id, - }), - [] + return ( + + + + + key='projectTable' + queryVariables={{ + filters: { searchTerm: debouncedSearch || undefined }, + }} + defaultSortOption={ProjectSortOption.OrganizationAndName} + queryDocument={GetProjectsDocument} + columns={PROJECT_COLUMNS} + noData='No projects' + pagePath='projects' + recordType='Project' + defaultFilterValues={{ + status: [ProjectFilterOptionStatus.Open], + }} + filters={projectFilters} + defaultPageSize={25} + /> + + ); - +}; +const OrganizationsTable = ({ + search, + setSearch, + debouncedSearch, +}: { + search: string; + setSearch: Dispatch>; + debouncedSearch?: string; +}) => { const isMobile = useIsMobile('sm'); + return ( + + + + + + + Add Organization + + + + + + + key='organizationTable' + queryDocument={GetOrganizationsDocument} + columns={ORGANIZATION_COLUMNS} + noData='No organizations' + pagePath='organizations' + recordType='Organization' + defaultPageSize={25} + queryVariables={{ + filters: { searchTerm: debouncedSearch || undefined }, + }} + noSort + /> + + + ); +}; + +const AllProjectsPage = () => { + const [viewMode, setViewMode] = useState('projects'); + const [search, setSearch, debouncedSearch] = useDebouncedState(''); + return ( { /> } > - - {viewMode === 'projects' && ( - - - - )} - {viewMode === 'organizations' && ( - - - - - - - Add Organization - - - - - - )} - - - {viewMode === 'projects' ? ( - - key='projectTable' - queryVariables={{ - filters: { searchTerm: debouncedSearch || undefined }, - }} - defaultSortOption={ProjectSortOption.OrganizationAndName} - queryDocument={GetProjectsDocument} - columns={PROJECT_COLUMNS} - rowLinkTo={projectRowLink} - noData='No projects' - pagePath='projects' - recordType='Project' - defaultFilterValues={{ - status: [ProjectFilterOptionStatus.Open], - }} - filters={projectFilters} - defaultPageSize={25} - /> - ) : ( - - key='organizationTable' - queryDocument={GetOrganizationsDocument} - columns={ORGANIZATION_COLUMNS} - rowLinkTo={organizationRowLink} - noData='No organizations' - pagePath='organizations' - recordType='Organization' - defaultPageSize={25} - queryVariables={{ - filters: { searchTerm: debouncedSearch || undefined }, - }} - noSort - /> - )} - - - + {viewMode === 'projects' ? ( + + ) : ( + + )} ); }; diff --git a/src/modules/projects/components/ProjectAssessments.tsx b/src/modules/projects/components/ProjectAssessments.tsx index daa5afdb8..5fde2713a 100644 --- a/src/modules/projects/components/ProjectAssessments.tsx +++ b/src/modules/projects/components/ProjectAssessments.tsx @@ -1,17 +1,22 @@ import { Paper } from '@mui/material'; import { useMemo } from 'react'; import { useProjectDashboardContext } from './ProjectDashboard'; -import { ColumnDef } from '@/components/elements/table/types'; +import TableRowActions from '@/components/elements/table/TableRowActions'; +import { + BASE_ACTION_COLUMN_DEF, + getViewAssessmentMenuItem, + getViewEnrollmentMenuItem, +} from '@/components/elements/table/tableRowActionUtil'; import PageTitle from '@/components/layout/PageTitle'; import useSafeParams from '@/hooks/useSafeParams'; import { + ASSESSMENT_CLIENT_NAME_COL, ASSESSMENT_COLUMNS, - ASSESSMENT_ENROLLMENT_COLUMNS, - assessmentRowLinkTo, } from '@/modules/assessments/util'; -import ClientName from '@/modules/client/components/ClientName'; import GenericTableWithData from '@/modules/dataFetching/components/GenericTableWithData'; import { useFilters } from '@/modules/hmis/filterUtil'; +import { assessmentDescription } from '@/modules/hmis/hmisUtil'; +import { WITH_ENROLLMENT_COLUMNS } from '@/modules/projects/components/tables/ProjectClientEnrollmentsTable'; import { AssessmentSortOption, GetProjectAssessmentsDocument, @@ -29,34 +34,39 @@ const ProjectAssessments = () => { }; const { project } = useProjectDashboardContext(); - const displayColumns: ColumnDef[] = useMemo(() => { + const columns = useMemo(() => { return [ + ASSESSMENT_CLIENT_NAME_COL, + ASSESSMENT_COLUMNS.date, + ASSESSMENT_COLUMNS.type, + WITH_ENROLLMENT_COLUMNS.entryDate, + WITH_ENROLLMENT_COLUMNS.exitDate, { - header: 'First Name', - linkTreatment: true, - render: (a: ProjectAssessmentType) => ( - - ), - }, - { - header: 'Last Name', - linkTreatment: true, - render: (a: ProjectAssessmentType) => ( - ( + ), }, - ASSESSMENT_COLUMNS.date, - ASSESSMENT_COLUMNS.type, - ASSESSMENT_ENROLLMENT_COLUMNS.period, ]; - }, []); - - const rowLinkTo = (record: ProjectAssessmentType) => - assessmentRowLinkTo(record, record.enrollment.client.id); + }, [project]); const filters = useFilters({ type: 'AssessmentsForProjectFilterOptions', @@ -74,9 +84,7 @@ const ProjectAssessments = () => { > queryVariables={{ id: projectId }} queryDocument={GetProjectAssessmentsDocument} - rowLinkTo={rowLinkTo} - rowLinkState={{ backToLabel: project.projectName }} - columns={displayColumns} + columns={columns} noData='No assessments' pagePath='project.assessments' recordType='Assessment' diff --git a/src/modules/projects/components/ProjectCurrentLivingSituations.tsx b/src/modules/projects/components/ProjectCurrentLivingSituations.tsx index 165ef1001..2a3dd9374 100644 --- a/src/modules/projects/components/ProjectCurrentLivingSituations.tsx +++ b/src/modules/projects/components/ProjectCurrentLivingSituations.tsx @@ -1,12 +1,17 @@ import { Paper } from '@mui/material'; - -import { useCallback, useMemo } from 'react'; +import TableRowActions from '@/components/elements/table/TableRowActions'; +import { + BASE_ACTION_COLUMN_DEF, + getViewEnrollmentMenuItem, +} from '@/components/elements/table/tableRowActionUtil'; +import { ColumnDef } from '@/components/elements/table/types'; import PageTitle from '@/components/layout/PageTitle'; import useSafeParams from '@/hooks/useSafeParams'; import ClientName from '@/modules/client/components/ClientName'; import GenericTableWithData from '@/modules/dataFetching/components/GenericTableWithData'; -import { baseColumns } from '@/modules/enrollment/components/pages/EnrollmentCurrentLivingSituationsPage'; -import EnrollmentDateRangeWithStatus from '@/modules/hmis/components/EnrollmentDateRangeWithStatus'; +import { CLS_COLUMNS } from '@/modules/enrollment/components/pages/EnrollmentCurrentLivingSituationsPage'; +import { clientBriefName, parseAndFormatDate } from '@/modules/hmis/hmisUtil'; +import { WITH_ENROLLMENT_COLUMNS } from '@/modules/projects/components/tables/ProjectClientEnrollmentsTable'; import { EnrollmentDashboardRoutes } from '@/routes/routes'; import { GetProjectCurrentLivingSituationsDocument, @@ -16,51 +21,47 @@ import { } from '@/types/gqlTypes'; import { generateSafePath } from '@/utils/pathEncoding'; +const COLUMNS: ColumnDef[] = [ + { + header: 'Client Name', + render: (cls: ProjectCurrentLivingSituationFieldsFragment) => ( + + ), + }, + CLS_COLUMNS.informationDate, + CLS_COLUMNS.livingSituation, + WITH_ENROLLMENT_COLUMNS.entryDate, + WITH_ENROLLMENT_COLUMNS.exitDate, + { + ...BASE_ACTION_COLUMN_DEF, + render: (cls) => ( + + ), + }, +]; + const ProjectCurrentLivingSituations = () => { const { projectId } = useSafeParams() as { projectId: string; }; - const columns = useMemo(() => { - return [ - { - header: 'First Name', - linkTreatment: true, - render: (cls: ProjectCurrentLivingSituationFieldsFragment) => ( - - ), - }, - { - header: 'Last Name', - linkTreatment: true, - render: (cls: ProjectCurrentLivingSituationFieldsFragment) => ( - - ), - }, - { ...baseColumns.informationDate, linkTreatment: false }, - baseColumns.livingSituation, - { - header: 'Enrollment Period', - render: (cls: ProjectCurrentLivingSituationFieldsFragment) => ( - - ), - }, - ]; - }, []); - - const rowLinkTo = useCallback( - (cls: ProjectCurrentLivingSituationFieldsFragment) => { - return generateSafePath( - EnrollmentDashboardRoutes.CURRENT_LIVING_SITUATIONS, - { - clientId: cls.client.id, - enrollmentId: cls.enrollment.id, - } - ); - }, - [] - ); - return ( <> @@ -72,11 +73,10 @@ const ProjectCurrentLivingSituations = () => { > queryVariables={{ id: projectId }} queryDocument={GetProjectCurrentLivingSituationsDocument} - columns={columns} + columns={COLUMNS} pagePath='project.currentLivingSituations' noData='No current living situations' recordType='CurrentLivingSituation' - rowLinkTo={rowLinkTo} /> diff --git a/src/modules/projects/components/tables/ProjectClientEnrollmentsTable.tsx b/src/modules/projects/components/tables/ProjectClientEnrollmentsTable.tsx index f0b0b65b5..6aaceab01 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 e0ca3664a..54f80aff7 100644 --- a/src/modules/projects/components/tables/ProjectExternalSubmissionsTable.tsx +++ b/src/modules/projects/components/tables/ProjectExternalSubmissionsTable.tsx @@ -1,7 +1,9 @@ -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'; +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'; @@ -102,15 +104,18 @@ const ProjectExternalSubmissionsTable = ({ }, ...defs, { - header: 'Action', - render: ({ id }: ExternalFormSubmissionSummaryFragment) => ( - + ...BASE_ACTION_COLUMN_DEF, + render: (submission: ExternalFormSubmissionSummaryFragment) => ( + setModalOpenId(submission.id), + disabled: bulkLoading, + }} + /> ), }, ]; diff --git a/src/modules/search/components/ClientSearch.tsx b/src/modules/search/components/ClientSearch.tsx index 41acba247..6605fc75b 100644 --- a/src/modules/search/components/ClientSearch.tsx +++ b/src/modules/search/components/ClientSearch.tsx @@ -14,9 +14,12 @@ import ClientSearchTypeToggle, { SearchType } from './ClientSearchTypeToggle'; import ClientTextSearchForm from './ClientTextSearchForm'; import ButtonLink from '@/components/elements/ButtonLink'; import { externalIdColumn } from '@/components/elements/ExternalIdDisplay'; -import RouterLink from '@/components/elements/RouterLink'; +import TableRowActions from '@/components/elements/table/TableRowActions'; +import { + BASE_ACTION_COLUMN_DEF, + getViewClientMenuItem, +} from '@/components/elements/table/tableRowActionUtil'; import { ColumnDef } from '@/components/elements/table/types'; -import { useIsMobile } from '@/hooks/useIsMobile'; import ClientName from '@/modules/client/components/ClientName'; import ClientSearchResultCard from '@/modules/client/components/ClientSearchResultCard'; @@ -30,13 +33,13 @@ import { import GenericTableWithData from '@/modules/dataFetching/components/GenericTableWithData'; import { SearchFormDefinition } from '@/modules/form/data'; import { useFilters } from '@/modules/hmis/filterUtil'; -import { clientNameAllParts } from '@/modules/hmis/hmisUtil'; +import { clientBriefName } from '@/modules/hmis/hmisUtil'; import { useHmisAppSettings } from '@/modules/hmisAppSettings/useHmisAppSettings'; import { isEnrollment, isHouseholdClient } from '@/modules/household/types'; import { RootPermissionsFilter } from '@/modules/permissions/PermissionsFilters'; import { useHasRootPermissions } from '@/modules/permissions/useHasPermissionsHooks'; -import { ClientDashboardRoutes, Routes } from '@/routes/routes'; +import { Routes } from '@/routes/routes'; import { ClientFieldsFragment, ClientSearchInput as ClientSearchInputType, @@ -49,7 +52,6 @@ import { SearchClientsQuery, SearchClientsQueryVariables, } from '@/types/gqlTypes'; -import { generateSafePath } from '@/utils/pathEncoding'; function asClient( record: @@ -70,20 +72,6 @@ export const CLIENT_COLUMNS: { | ProjectEnrollmentsHouseholdClientFieldsFragment >; } = { - id: { header: 'HMIS ID', render: 'id' }, - linkedId: { - header: 'ID', - render: (client) => ( - - {client.id} - - ), - }, name: { header: 'Name', key: 'name', @@ -94,11 +82,6 @@ export const CLIENT_COLUMNS: { key: 'age', render: (client) => asClient(client).age, }, - linkedName: { - header: 'Name', - key: 'name', - render: (client) => , - }, linkedNameNewTab: { header: 'Name', key: 'name', @@ -138,27 +121,19 @@ 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' }, -]; - -export const MOBILE_SEARCH_RESULT_COLUMNS: ColumnDef[] = [ - CLIENT_COLUMNS.id, +const SEARCH_RESULT_COLUMNS: ColumnDef[] = [ + CLIENT_COLUMNS.name, + CLIENT_COLUMNS.age, { - ...CLIENT_COLUMNS.name, - linkTreatment: true, - ariaLabel: (row) => clientNameAllParts(row), + ...BASE_ACTION_COLUMN_DEF, + render: (client) => ( + + ), }, - { ...CLIENT_COLUMNS.ssn }, - { ...CLIENT_COLUMNS.dobAge }, ]; /** @@ -179,8 +154,6 @@ const ClientSearch = () => { // whether search has occurred const [hasSearched, setHasSearched] = useState(false); - const isMobile = useIsMobile(); - const [searchInput, setSearchInput] = useState( null ); @@ -198,22 +171,15 @@ const ClientSearch = () => { if (displayType === 'cards') { return []; } - let baseColumns = isMobile - ? MOBILE_SEARCH_RESULT_COLUMNS - : SEARCH_RESULT_COLUMNS; + let baseColumns = SEARCH_RESULT_COLUMNS; if (globalFeatureFlags?.mciId) { baseColumns = [ externalIdColumn(ExternalIdentifierType.MciId, 'MCI ID'), ...baseColumns, ]; } - if (!canViewSsn) baseColumns = baseColumns.filter((c) => c.key !== 'ssn'); - if (!canViewDob) - baseColumns = baseColumns.map((c) => - c.key === 'dob' ? { ...c, header: 'Age' } : c - ); return baseColumns; - }, [isMobile, globalFeatureFlags, displayType, canViewSsn, canViewDob]); + }, [globalFeatureFlags, displayType]); useEffect(() => { // if search params are derived, we don't want to perform a search on them @@ -234,14 +200,6 @@ const ClientSearch = () => { } }, [derivedSearchParams, searchParams]); - const rowLinkTo = useCallback( - (row: ClientFieldsFragment) => - generateSafePath(ClientDashboardRoutes.PROFILE, { - clientId: row.id, - }), - [] - ); - const onClearSearch = useCallback(() => { setSearchInput(null); setSearchParams({}); @@ -327,7 +285,6 @@ const ClientSearch = () => { queryVariables={{ input: searchInput }} queryDocument={SearchClientsDocument} onCompleted={() => setHasSearched(true)} - rowLinkTo={rowLinkTo} columns={columns} pagePath='clientSearch' fetchPolicy='cache-and-network' diff --git a/src/modules/services/components/ClientServicesPage.tsx b/src/modules/services/components/ClientServicesPage.tsx index d814a1f1d..fb590d088 100644 --- a/src/modules/services/components/ClientServicesPage.tsx +++ b/src/modules/services/components/ClientServicesPage.tsx @@ -1,26 +1,31 @@ import { Paper } from '@mui/material'; -import { useMemo } from 'react'; +import React, { useMemo } from 'react'; -import RouterLink from '@/components/elements/RouterLink'; +import TableRowActions from '@/components/elements/table/TableRowActions'; +import { + BASE_ACTION_COLUMN_DEF, + getViewEnrollmentMenuItem, + getViewServiceMenuItem, +} from '@/components/elements/table/tableRowActionUtil'; import { ColumnDef } from '@/components/elements/table/types'; import PageTitle from '@/components/layout/PageTitle'; import useSafeParams from '@/hooks/useSafeParams'; +import useClientDashboardContext from '@/modules/client/hooks/useClientDashboardContext'; import GenericTableWithData from '@/modules/dataFetching/components/GenericTableWithData'; -import EnrollmentDateRangeWithStatus from '@/modules/hmis/components/EnrollmentDateRangeWithStatus'; import { useFilters } from '@/modules/hmis/filterUtil'; +import { entryExitRange, parseAndFormatDate } from '@/modules/hmis/hmisUtil'; import { + getServiceTypeForDisplay, SERVICE_BASIC_COLUMNS, SERVICE_COLUMNS, } from '@/modules/services/serviceColumns'; -import { EnrollmentDashboardRoutes } from '@/routes/routes'; import { GetClientServicesDocument, GetClientServicesQuery, GetClientServicesQueryVariables, ServiceSortOption, } from '@/types/gqlTypes'; -import { generateSafePath } from '@/utils/pathEncoding'; type ServiceType = NonNullable< NonNullable['services'] @@ -31,53 +36,52 @@ const ClientServicesPage: React.FC<{ enrollmentId?: string; }> = ({ omitColumns = [] }) => { const { clientId } = useSafeParams() as { clientId: string }; + const { client } = useClientDashboardContext(); const columns = useMemo( () => ( [ - SERVICE_BASIC_COLUMNS.dateProvided, + SERVICE_BASIC_COLUMNS.serviceDate, SERVICE_BASIC_COLUMNS.serviceType, { key: 'project', header: 'Project Name', - render: (row) => ( - - {row.enrollment.projectName} - - ), - }, - { - key: 'en-period', - header: 'Enrollment Period', - optional: true, - render: (row) => ( - - ), + render: (row) => row.enrollment.projectName, }, { ...SERVICE_COLUMNS.serviceDetails, optional: true, defaultHidden: true, }, + { + ...BASE_ACTION_COLUMN_DEF, + render: (row) => ( + + ), + }, ] as ColumnDef[] ).filter((col) => { if (omitColumns.includes(col.key || '')) return false; return true; }), - [clientId, omitColumns] + [client, clientId, omitColumns] ); const filters = useFilters({ diff --git a/src/modules/services/components/EnrollmentServicesPage.tsx b/src/modules/services/components/EnrollmentServicesPage.tsx index bcbda40f9..3974830f9 100644 --- a/src/modules/services/components/EnrollmentServicesPage.tsx +++ b/src/modules/services/components/EnrollmentServicesPage.tsx @@ -1,14 +1,18 @@ import AddIcon from '@mui/icons-material/Add'; import { Button } from '@mui/material'; -import { useState } from 'react'; +import { useMemo, useState } from 'react'; +import TableRowActions from '@/components/elements/table/TableRowActions'; +import { BASE_ACTION_COLUMN_DEF } from '@/components/elements/table/tableRowActionUtil'; import TitleCard from '@/components/elements/TitleCard'; import NotFound from '@/components/pages/NotFound'; import GenericTableWithData from '@/modules/dataFetching/components/GenericTableWithData'; import useEnrollmentDashboardContext from '@/modules/enrollment/hooks/useEnrollmentDashboardContext'; import { useFilters } from '@/modules/hmis/filterUtil'; +import { parseAndFormatDate } from '@/modules/hmis/hmisUtil'; import { useServiceDialog } from '@/modules/services/hooks/useServiceDialog'; import { + getServiceTypeForDisplay, SERVICE_BASIC_COLUMNS, SERVICE_COLUMNS, } from '@/modules/services/serviceColumns'; @@ -43,16 +47,39 @@ const EnrollmentServicesPage = () => { DataCollectionFeatureRole.Service ); - if (!enrollment || !enrollmentId || !clientId || !serviceFeature) - return ; + const canEditServices = enrollment?.access.canEditEnrollments; - const canEditServices = enrollment.access.canEditEnrollments; + const columns = useMemo(() => { + return [ + SERVICE_BASIC_COLUMNS.serviceDate, + SERVICE_BASIC_COLUMNS.serviceType, + SERVICE_COLUMNS.serviceDetails, + ...(canEditServices + ? [ + { + ...BASE_ACTION_COLUMN_DEF, + render: (service: ServiceFieldsFragment) => ( + { + setViewingRecord(service); + openServiceDialog(); + }, + }} + /> + ), + }, + ] + : []), + ]; + }, [canEditServices, openServiceDialog]); - const columns = [ - SERVICE_BASIC_COLUMNS.dateProvided, - SERVICE_BASIC_COLUMNS.serviceType, - SERVICE_COLUMNS.serviceDetails, - ]; + if (!enrollment || !enrollmentId || !clientId || !serviceFeature) + return ; return ( <> @@ -78,14 +105,6 @@ const EnrollmentServicesPage = () => { GetEnrollmentServicesQueryVariables, ServiceFieldsFragment > - handleRowClick={ - canEditServices - ? (record) => { - setViewingRecord(record); - openServiceDialog(); - } - : undefined - } queryVariables={{ id: enrollmentId }} queryDocument={GetEnrollmentServicesDocument} columns={columns} diff --git a/src/modules/services/components/ProjectServicesTable.tsx b/src/modules/services/components/ProjectServicesTable.tsx index b7830673a..ebb11f65b 100644 --- a/src/modules/services/components/ProjectServicesTable.tsx +++ b/src/modules/services/components/ProjectServicesTable.tsx @@ -1,20 +1,28 @@ -import { useCallback, useMemo } from 'react'; +import { useMemo } from 'react'; +import TableRowActions from '@/components/elements/table/TableRowActions'; +import { + BASE_ACTION_COLUMN_DEF, + getViewEnrollmentMenuItem, + getViewServiceMenuItem, +} from '@/components/elements/table/tableRowActionUtil'; import { ColumnDef } from '@/components/elements/table/types'; import ClientName from '@/modules/client/components/ClientName'; import GenericTableWithData from '@/modules/dataFetching/components/GenericTableWithData'; -import EnrollmentDateRangeWithStatus from '@/modules/hmis/components/EnrollmentDateRangeWithStatus'; import { useFilters } from '@/modules/hmis/filterUtil'; -import { SERVICE_BASIC_COLUMNS } from '@/modules/services/serviceColumns'; -import { EnrollmentDashboardRoutes } from '@/routes/routes'; +import { clientBriefName, parseAndFormatDate } from '@/modules/hmis/hmisUtil'; +import { WITH_ENROLLMENT_COLUMNS } from '@/modules/projects/components/tables/ProjectClientEnrollmentsTable'; +import { + getServiceTypeForDisplay, + SERVICE_BASIC_COLUMNS, +} from '@/modules/services/serviceColumns'; import { GetProjectServicesDocument, GetProjectServicesQuery, GetProjectServicesQueryVariables, ServicesForProjectFilterOptions, } from '@/types/gqlTypes'; -import { generateSafePath } from '@/utils/pathEncoding'; export type ServiceFields = NonNullable< GetProjectServicesQuery['project'] @@ -31,25 +39,33 @@ const ProjectServicesTable = ({ if (columns) return columns; return [ { - header: 'First Name', - linkTreatment: true, - render: (s: ServiceFields) => ( - - ), - }, - { - header: 'Last Name', - linkTreatment: true, + header: 'Client Name', render: (s: ServiceFields) => ( - + ), }, - { ...SERVICE_BASIC_COLUMNS.dateProvided, linkTreatment: false }, + { ...SERVICE_BASIC_COLUMNS.serviceDate, linkTreatment: false }, SERVICE_BASIC_COLUMNS.serviceType, + WITH_ENROLLMENT_COLUMNS.entryDate, + WITH_ENROLLMENT_COLUMNS.exitDate, { - header: 'Enrollment Period', - render: (s: ServiceFields) => ( - + ...BASE_ACTION_COLUMN_DEF, + render: (service: ServiceFields) => ( + ), }, ]; @@ -59,13 +75,6 @@ const ProjectServicesTable = ({ type: 'ServicesForProjectFilterOptions', }); - const rowLinkTo = useCallback((s: ServiceFields) => { - return generateSafePath(EnrollmentDashboardRoutes.SERVICES, { - clientId: s.enrollment.client.id, - enrollmentId: s.enrollment.id, - }); - }, []); - return ( ); }; diff --git a/src/modules/services/serviceColumns.tsx b/src/modules/services/serviceColumns.tsx index 5c8640d74..3a0899ab6 100644 --- a/src/modules/services/serviceColumns.tsx +++ b/src/modules/services/serviceColumns.tsx @@ -6,24 +6,31 @@ import { parseAndFormatDate, serviceDetails } from '@/modules/hmis/hmisUtil'; import { ServiceBasicFieldsFragment, ServiceFieldsFragment, + ServiceTypeConfigFieldsFragment, } from '@/types/gqlTypes'; +export const getServiceTypeForDisplay = ( + serviceType?: Pick< + ServiceTypeConfigFieldsFragment, + 'name' | 'serviceCategory' + > | null +) => { + if (!serviceType) return 'Unknown Service'; + const { name, serviceCategory } = serviceType; + if (name === serviceCategory.name) return name; + return `${serviceCategory.name} - ${name}`; +}; + export const SERVICE_BASIC_COLUMNS: { [key: string]: ColumnDef; } = { - dateProvided: { - header: 'Date Provided', - linkTreatment: true, + serviceDate: { + header: 'Service Date', render: (s) => parseAndFormatDate(s.dateProvided), }, serviceType: { header: 'Service Type', - render: ({ serviceType }) => { - if (!serviceType) return 'Unknown Service'; - const { name, serviceCategory } = serviceType; - if (name === serviceCategory.name) return name; - return `${serviceCategory.name} - ${name}`; - }, + render: (s) => getServiceTypeForDisplay(s.serviceType), }, };