Skip to content

Commit

Permalink
Merge branch 'detail-page' into aidbox-forms
Browse files Browse the repository at this point in the history
  • Loading branch information
ir4y committed Jan 29, 2025
2 parents 7936efb + c889d1c commit e7c92af
Show file tree
Hide file tree
Showing 10 changed files with 454 additions and 14 deletions.
Original file line number Diff line number Diff line change
@@ -1,15 +1,24 @@
import { S } from "./styles";

export type PageContainerContentProps = React.HTMLAttributes<HTMLDivElement> & {
/* Page content max width */
maxWidth?: number | string;

/**
* PageContainerContent (level=2) can be nested in other PageContainerContent (level=1)
* e.g. ResourceListPageContent use PageContainerContent (level=2)
*
* Styles of the PageContainerContent will be adjusted for provided level.
*/
level?: 1 | 2;
};

export function PageContainerContent(props: PageContainerContentProps) {
const { maxWidth, ...rest } = props;
const { maxWidth, level = 1, ...rest } = props;

return (
<S.PageContentContainer>
<S.PageContent {...rest} $maxWidth={maxWidth} />
<S.PageContentContainer $level={level}>
<S.PageContent {...rest} $maxWidth={maxWidth} $level={level} />
</S.PageContentContainer>
);
}
Original file line number Diff line number Diff line change
@@ -1,22 +1,34 @@
import styled from 'styled-components';
import styled, { css } from 'styled-components';

import { maxWidthStyles } from '../PageContainerHeader/styles';

export const S = {
PageContentContainer: styled.div`
PageContentContainer: styled.div<{ $level: 1 | 2 }>`
padding: 0 24px;
display: flex;
flex-direction: column;
align-items: center;
${({ $level }) =>
$level === 2 &&
css`
padding: 0;
`}
`,
PageContent: styled.div<{ $maxWidth?: number | string }>`
PageContent: styled.div<{ $level: 1 | 2, $maxWidth?: number | string }>`
flex: 1;
display: flex;
flex-direction: column;
padding: 32px 0;
gap: 24px 0;
width: 100%;
${({ $level }) =>
$level === 2 &&
css`
padding: 0;
`}
${maxWidthStyles}
`,
};
5 changes: 3 additions & 2 deletions src/components/SearchBar/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,11 @@ import { useMemo } from 'react';

interface SearchBarProps extends SearchBarData {
showInDrawerOnMobile?: boolean;
level?: 1 | 2;
}

export function SearchBar(props: SearchBarProps) {
const { columnsFilterValues, onChangeColumnFilter, onResetFilters, showInDrawerOnMobile = true } = props;
const { columnsFilterValues, onChangeColumnFilter, onResetFilters, showInDrawerOnMobile = true, level = 1 } = props;
const searchBarFilterValues = useMemo(
() => columnsFilterValues.filter((filter) => isSearchBarFilter(filter)),
[JSON.stringify(columnsFilterValues)],
Expand All @@ -36,7 +37,7 @@ export function SearchBar(props: SearchBarProps) {
<Trans>Clear filters</Trans>
</Button>
</S.SearchBar>
<S.MobileFilters $showInDrawerOnMobile={showInDrawerOnMobile}>
<S.MobileFilters $showInDrawerOnMobile={showInDrawerOnMobile} $level={level}>
<SearchBarMobile {...props} />
</S.MobileFilters>
</>
Expand Down
9 changes: 8 additions & 1 deletion src/components/SearchBar/styles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,18 @@ export const S = {
}
`}
`,
MobileFilters: styled.div<{ $showInDrawerOnMobile?: boolean }>`
MobileFilters: styled.div<{ $level: 1 | 2; $showInDrawerOnMobile?: boolean }>`
position: absolute;
right: 24px;
top: 24px;
${({ $level }) =>
$level === 2 &&
css`
right: 0;
top: 0;
`}
${({ $showInDrawerOnMobile }) =>
$showInDrawerOnMobile &&
css`
Expand Down
6 changes: 4 additions & 2 deletions src/containers/App/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import { Spinner } from 'src/components/Spinner';
import { PublicAppointment } from 'src/containers/Appointment/PublicAppointment';
import { EncounterList } from 'src/containers/EncounterList';
import { PatientDetails } from 'src/containers/PatientDetails';
import { NewPatientDetails } from 'src/containers/PatientDetails/new';
import { PatientList } from 'src/containers/PatientList';
import { PatientQuestionnaire } from 'src/containers/PatientQuestionnaire';
import { PractitionerDetails } from 'src/containers/PractitionerDetails';
Expand All @@ -25,6 +26,7 @@ import { SignIn } from 'src/containers/SignIn';
import { VideoCall } from 'src/containers/VideoCall';
import { getToken, parseOAuthState, setToken } from 'src/services/auth';

import { DefaultUserWithNoRoles } from './DefaultUserWithNoRoles';
import { restoreUserSession } from './utils';
import { AidboxFormsBuilder } from '../AidboxFormsBuilder';
import { HealthcareServiceList } from '../HealthcareServiceList';
Expand All @@ -34,10 +36,9 @@ import { MedicationManagement } from '../MedicationManagement';
import { NotificationPage } from '../NotificationPage';
import { OrganizationScheduling } from '../OrganizationScheduling';
import { DocumentPrint } from '../PatientDetails/DocumentPrint';
import { PatientResourceListExample } from '../PatientResourceListExample';
import { Prescriptions } from '../Prescriptions';
import { SetPassword } from '../SetPassword';
import { PatientResourceListExample } from '../PatientResourceListExample';
import { DefaultUserWithNoRoles } from './DefaultUserWithNoRoles';

interface AppProps {
authenticatedRoutes?: ReactElement;
Expand Down Expand Up @@ -169,6 +170,7 @@ function AuthenticatedUserApp({ defaultRoute, extra }: RouteProps) {
<Route path="/patients" element={<PatientList />} />
<Route path="/patients-uber" element={<PatientResourceListExample />} />
<Route path="/patients/:id/*" element={<PatientDetails />} />
<Route path="/patients2/:id/*" element={<NewPatientDetails />} />
<Route path="/questionnaire" element={<PatientQuestionnaire />} />
<Route path="/documents/:id/edit" element={<div>documents/:id/edit</div>} />
<Route path="/encounters/:encounterId/video" element={<VideoCall />} />
Expand Down
115 changes: 115 additions & 0 deletions src/containers/PatientDetails/new.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
import { Encounter, Patient } from 'fhir/r4b';

import { DetailPage, Tab } from 'src/uberComponents/DetailPage';
import { compileAsFirst, formatPeriodDateTime } from 'src/utils';

import { PatientOverview } from './PatientOverviewDynamic';
import { ResourceListPageContent } from 'src/uberComponents/ResourceListPageContent';
import { navigationAction, questionnaireAction } from 'src/uberComponents';
import { t, Trans } from '@lingui/macro';
import { SearchBarColumnType } from 'src/components/SearchBar/types';
import { PlusOutlined } from '@ant-design/icons';

const getName = compileAsFirst<Patient, string>("Patient.name.given.first() + ' ' + Patient.name.family");

function PatientEncounter({ patient }: { patient: Patient }) {
return (
<ResourceListPageContent<Encounter>
resourceType="Encounter"
searchParams={{ patient: patient.id! }}
getTableColumns={() => [
{
title: 'Practitioner',
dataIndex: 'practitioner',
key: 'practitioner',
render: (_text: any, { resource }) => resource.participant?.[0]?.individual?.display,
},
{
title: 'Status',
dataIndex: 'status',
key: 'status',
render: (_text: any, { resource }) => {
return resource.status;
},
},
{
title: 'Date',
dataIndex: 'date',
key: 'date',
width: 250,
render: (_text: any, { resource }) => formatPeriodDateTime(resource.period),
},
]}
getFilters={() => [
{
id: 'name',
searchParam: '_ilike',
type: SearchBarColumnType.STRING,
placeholder: t`Find encounter`,
placement: ['search-bar', 'table'],
},
{
id: 'status',
searchParam: 'status',
type: SearchBarColumnType.CHOICE,
placeholder: t`Choose status`,
options: [
{
value: {
Coding: {
code: 'in-progress',
display: 'In progress',
},
},
},
{
value: {
Coding: {
code: 'finished',
display: 'Finished',
},
},
},
],
placement: ['table', 'search-bar'],
},
]}
getRecordActions={(record) => [navigationAction('Open', `/patients2/${record.resource.id}/encounter`)]}
getHeaderActions={() => [
questionnaireAction(<Trans>Create encounter</Trans>, 'encounter-create', { icon: <PlusOutlined /> }),
]}
getBatchActions={() => [questionnaireAction(<Trans>Finish encounters</Trans>, '')]}
getReportColumns={(bundle) => [
{
title: t`Number of Encounters`,
value: bundle.total,
},
]}
/>
);
}

const tabs: Array<Tab<Patient>> = [
{
path: '',
label: 'Overview',
component: ({ resource }) => <PatientOverview patient={resource} />,
},
{
path: 'encounter',
label: 'Encounter',
component: ({ resource }) => <PatientEncounter patient={resource} />,
},
];

export function NewPatientDetails() {
return (
<DetailPage<Patient>
resourceType="Patient"
getSearchParams={({ id }) => ({ _id: id })}
getTitle={({ resource, bundle }) => getName(resource, { bundle })!}
tabs={tabs}
basePath="patients2"
/>
);
}
109 changes: 109 additions & 0 deletions src/uberComponents/DetailPage/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
import { Bundle, Resource } from 'fhir/r4b';
import { useEffect, useState } from 'react';
import { useParams, Route, Routes, useLocation, Link, useNavigate, Params } from 'react-router-dom';

import { SearchParams, RenderRemoteData, useService, WithId } from '@beda.software/fhir-react';

import { PageContainer } from 'src/components';
import { RouteItem } from 'src/components/BaseLayout/Sidebar/SidebarTop';
import { Tabs } from 'src/components/Tabs';
import { getFHIRResources } from 'src/services';
import { compileAsFirst } from 'src/utils';

interface DetailContext<R extends Resource> {
resource: R;
bundle: Bundle<R>;
}

export interface Tab<R extends Resource> {
label: string;
path?: string;
component: (context: DetailContext<R>) => JSX.Element;
}

interface DetailPageProps<R extends Resource> {
resourceType: R['resourceType'];
getSearchParams: (params: Readonly<Params<string>>) => SearchParams;
getTitle: (context: DetailContext<R>) => string;
tabs: Array<Tab<R>>;
basePath: string;
extractPrimaryResource?: (bundle: Bundle<R>) => R;
}

interface PageTabsProps<R extends Resource> {
tabs: Array<Tab<R>>;
basePath: string;
}

export function PageTabs<R extends Resource>({ tabs, basePath }: PageTabsProps<R>) {
const location = useLocation();
const params = useParams<{ id: string }>();
const navigate = useNavigate();

const menuItems: RouteItem[] = tabs.map(({ label, path }) => ({
label,
path: `/${basePath}/${params.id}/${path}`,
}));

const [currentPath, setCurrentPath] = useState(location?.pathname);

useEffect(() => {
setCurrentPath(location?.pathname);
}, [location]);

return (
<Tabs
boxShadow={false}
activeKey={currentPath.split('/').slice(0, 4).join('/')}
items={menuItems.map((route) => ({
key: route.path,
label: <Link to={route.path}>{route.label}</Link>,
}))}
onTabClick={(path) => navigate(path)}
/>
);
}

export function DetailPage<R extends Resource>({
resourceType,
getSearchParams,
getTitle,
tabs,
basePath,
extractPrimaryResource,
}: DetailPageProps<R>) {
const params = useParams();
const [response] = useService(() => getFHIRResources(resourceType, getSearchParams(params)));
const defaultExtractPrimaryResource = compileAsFirst<Bundle<WithId<R>>, R>(
'Bundle.entry.resource.where(resourceType=%resourceType).first()',
);
return (
<RenderRemoteData remoteData={response}>
{(bundle) => {
let resource: R | undefined = undefined;
if (extractPrimaryResource) {
resource = extractPrimaryResource(bundle);
} else {
resource = defaultExtractPrimaryResource(bundle, { resourceType });
}
if (typeof resource === 'undefined') {
return <p>NASTY ERROR</p>;
}
const context: DetailContext<R> = { resource, bundle };
return (
<PageContainer
title={getTitle(context)}
layoutVariant="with-tabs"
headerContent={<PageTabs tabs={tabs} basePath={basePath} />}
>
<Routes>
{tabs.map(({ path, component }) => (
<Route path={'/' + path} element={component(context)} key={path} />
))}
</Routes>
</PageContainer>
);
}}
</RenderRemoteData>
);
}
6 changes: 3 additions & 3 deletions src/uberComponents/ResourceListPage/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ interface ReportColumn {
value: React.ReactNode;
}

interface ResourceListPageProps<R extends Resource> {
export interface ResourceListPageProps<R extends Resource> {
/* Page header title (for example, Organizations) */
headerTitle: string;

Expand Down Expand Up @@ -246,7 +246,7 @@ interface ResourcesListPageReportProps<R> {
getReportColumns: (bundle: Bundle, reportBundle?: Bundle) => Array<ReportColumn>;
}

function ResourcesListPageReport<R>(props: ResourcesListPageReportProps<R>) {
export function ResourcesListPageReport<R>(props: ResourcesListPageReportProps<R>) {
const { recordResponse, getReportColumns } = props;
const emptyBundle: Bundle = { resourceType: 'Bundle', entry: [], type: 'searchset' };
const items =
Expand All @@ -257,7 +257,7 @@ function ResourcesListPageReport<R>(props: ResourcesListPageReportProps<R>) {
return <Report items={items} />;
}

function getRecordActionsColumn<R extends Resource>({
export function getRecordActionsColumn<R extends Resource>({
getRecordActions,
defaultLaunchContext,
reload,
Expand Down
Loading

0 comments on commit e7c92af

Please sign in to comment.