Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

(feat) O3-3018: Adding metric tiles to the refApp homepage. #1075

Open
wants to merge 20 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 17 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import React from 'react';
import { Tile } from '@carbon/react';
import styles from '../homepage-tiles.scss';
import { useTranslation } from 'react-i18next';
import useActiveVisits from './active-visits-tile.resources';

const ActiveVisitsTile: React.FC = () => {
const { data: activeVisitsData } = useActiveVisits();

const { t } = useTranslation();
return (
<React.Fragment>
<Tile className={styles.tileContainer}>
<div>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This tag seems redundant

<div className={styles.tileContent}>
<div className={styles.tileHeader}>
<header>{t('activeVisits', 'Active Visits')}</header>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we just move the className to the header and eliminate the need for the extra

</div>
<div className={styles.displayDetails}>
<div className={styles.countLabel}>Patients</div>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add translation for Patients

<div className={styles.displayData}>{activeVisitsData?.length ?? 0}</div>
</div>
</div>
</div>
</Tile>
</React.Fragment>
);
};

export default ActiveVisitsTile;
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { openmrsFetch, useSession } from '@openmrs/esm-framework';
import useSWR from 'swr';

export default function useActiveVisits() {
const session = useSession();
const sessionLocation = session?.sessionLocation?.uuid;

const customRepresentation = 'custom:(uuid,startDatetime,stopDatetime)';

const getUrl = () => {
let url = `/ws/rest/v1/visit?v=${customRepresentation}&`;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
let url = `/ws/rest/v1/visit?v=${customRepresentation}&`;
let url = `${restBaseUrl}/visit?v=${customRepresentation}&`;

let urlSearchParams = new URLSearchParams();

urlSearchParams.append('includeInactive', 'false');
urlSearchParams.append('totalCount', 'true');
urlSearchParams.append('location', `${sessionLocation}`);

return url + urlSearchParams.toString();
};

const { data, error, isLoading } = useSWR<{ data: { results: any[]; totalCount: number } }>(getUrl, openmrsFetch);

return {
data: data?.data.results,
error,
isLoading,
};
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
@use '@carbon/type';
@use '@carbon/colors';
@import '~@openmrs/esm-styleguide/src/vars';

.tileContent {
display: flex;
flex-direction: column;
align-items: space-between;
}

.tileContainer {
background-color: white;
border: 0.0625rem solid #e0e0e0;
height: 7.875rem;
padding: 1rem;
margin: 0.5rem 0.5rem;
}

.tileHeader {
font-size: 0.875rem;
font-weight: 600;
line-height: 1.28572;
letter-spacing: 0.16px;
color: #525252;
}

.displayDetails {
margin-top: 1.5rem;
}

.displayData {
font-size: 1.75rem;
font-weight: 400;
line-height: 1.28572;
letter-spacing: 0;
color: #161616;
}

.countLabel {
@include type.type-style('label-01');
color: $text-02;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { ExtensionSlot } from '@openmrs/esm-framework';
import React from 'react';
import styles from './metrics-slot.scss';

const MetricsSlot = () => {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This becomes tricky when we're adding tiles from other esms. Can we maintain this in the esm home?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My though process behind this:

  • We need a way to manage the parent container of the metric tiles that does not rely on openmrs-esm-home since that slot should be totally generic and agnostic of whatever is loaded into it.
  • This works since the different tiles reside within different esms (esm-active-visits-app, esm-appointments-app) but need to live inside a single container for the purposes of uniformity of styling/ alignment, and also so that they are loaded into the extension slot within openmrs-esm-home as a single unit instead of three individual tiles.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is my understanding;

  • This component(MetricSlot) introduces the Metrics slot which is specific to the home.
  • Different esm can then define tiles that are attached to this slot.

What am suggesting is this specific component defining the metrics-slot should be esm-home and patient-management let alone the esm-visits-app. It's not a tied to the visits esm and implementers who decide not to include the esm-visits in the importmap would have this. But implementations that have esm-home should be able to take advantage of the metrics-tiles-slot

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Aah, I see what you mean! Which begs the question, where within patient-management would you suggest I define this? Could there be a way to represent this logic independent of any of the modules? I hope my understanding of what you said was correct.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The slot would move to the esm-home and the different tiles would be attached in the routes.json of their respective esms

return <ExtensionSlot name="metrics-extension-slot" className={styles.extensionSlot} />;
};

export default MetricsSlot;
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
.extensionSlot {
display: grid;
grid-template-columns: 1fr 1fr 1fr;
margin: 0 0.5rem;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import React from 'react';
import { Tile } from '@carbon/react';
import styles from '../homepage-tiles.scss';
import { useTranslation } from 'react-i18next';
import useTotalVisits from './total-visits-tile.resources';

const TotalVisitsTile: React.FC = () => {
const { data: appointmentsData } = useTotalVisits();

const { t } = useTranslation();

return (
<React.Fragment>
<Tile className={styles.tileContainer}>
<div>
<div className={styles.tileContent}>
<div className={styles.tileHeader}>
<header>{t('totalVisits', 'Total Visits Today')}</header>
</div>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same comments apply to this component about cleaning this up

<div className={styles.displayDetails}>
<div className={styles.countLabel}>Patients</div>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Translations

<div className={styles.displayData}>{appointmentsData?.length ?? 0}</div>
</div>
</div>
</div>
</Tile>
</React.Fragment>
);
};

export default TotalVisitsTile;
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { openmrsFetch } from '@openmrs/esm-framework';
import useSWR from 'swr';
import dayjs from 'dayjs';
import { type Visit } from '../../types/index';

const useTotalVisits = () => {
const omrsDateFormat = 'YYYY-MM-DDTHH:mm:ss.SSSZZ';
const currentVisitDate = dayjs(new Date().setHours(0, 0, 0, 0)).format(omrsDateFormat);
const customRepresentation = 'custom:(uuid,startDatetime,stopDatetime)';

const visitsUrl = `/ws/rest/v1/visit?includeInactive=true&v=${customRepresentation}&fromStartDate=${dayjs(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
const visitsUrl = `/ws/rest/v1/visit?includeInactive=true&v=${customRepresentation}&fromStartDate=${dayjs(
const visitsUrl = `${restBaseUrl}/visit?includeInactive=true&v=${customRepresentation}&fromStartDate=${dayjs(

currentVisitDate,
).format('YYYY-MM-DD')}`;

const { data, error, isLoading } = useSWR<{ data: { results: Visit[] } }>(visitsUrl, openmrsFetch);

const responseData = data?.data.results;

return { data: responseData, error, isLoading };
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
const responseData = data?.data.results;
return { data: responseData, error, isLoading };
return { data: data?.data.results, error, isLoading };

};

export default useTotalVisits;
14 changes: 13 additions & 1 deletion packages/esm-active-visits-app/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { defineConfigSchema, getSyncLifecycle } from '@openmrs/esm-framework';
import { defineConfigSchema, getAsyncLifecycle, getSyncLifecycle } from '@openmrs/esm-framework';
import { configSchema } from './config-schema';
import activeVisitsComponent from './active-visits-widget/active-visits.component';
import visitDetailComponent from './visits-summary/visit-detail.component';
Expand All @@ -16,6 +16,18 @@ export function startupApp() {
defineConfigSchema(moduleName, configSchema);
}

export const homePageTilesSlot = getAsyncLifecycle(() => import('./home-page-tiles/metrics-slot.component'), options);

export const activeVisits = getSyncLifecycle(activeVisitsComponent, options);

export const visitDetail = getSyncLifecycle(visitDetailComponent, options);

export const homeActiveVisitsTile = getAsyncLifecycle(
() => import('./home-page-tiles/active-visits-metric-tile/active-visits-tile.component'),
options,
);

export const homeTotalVisitsTile = getAsyncLifecycle(
() => import('./home-page-tiles/total-visits-metric-tile/total-visits-tile.component'),
options,
);
15 changes: 15 additions & 0 deletions packages/esm-active-visits-app/src/routes.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,21 @@
"webservices.rest": "^2.2.0"
},
"extensions": [
{
"name": "homepage-tiles",
"slot": "home-metrics-tiles-slot",
"component": "homePageTilesSlot"
},
{
"name": "home-active-visits-tile",
"slot": "metrics-extension-slot",
"component": "homeActiveVisitsTile"
},
{
"name": "home-total-visits-tile",
"slot": "metrics-extension-slot",
"component": "homeTotalVisitsTile"
},
{
"name": "active-visits-widget",
"slot": "homepage-widgets-slot",
Expand Down
15 changes: 15 additions & 0 deletions packages/esm-active-visits-app/src/types/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { type OpenmrsResource } from '@openmrs/esm-framework';

export interface SearchedPatient {
patientId: number;
uuid: string;
Expand Down Expand Up @@ -26,3 +28,16 @@ export interface Identifier {
preferred: boolean;
voided: boolean;
}

export interface Visit {
uuid: string;
display?: string;
encounters: Array<OpenmrsResource>;
patient?: OpenmrsResource;
visitType: OpenmrsResource;
location?: OpenmrsResource;
startDatetime: string;
stopDatetime?: string;
attributes?: Array<OpenmrsResource>;
[anythingElse: string]: any;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Was this a placeholder?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are you referring to: [anythingElse: string]: any;?
If so then this was just a contingency.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it's best not to have this. That forces us to confront any short-comings in the types instead of ignoring them... and if someone really needs to use a different property, there's always (visit as any).prop = value as a work-around.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for the clarity, will resolve this.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's delete this

}
1 change: 1 addition & 0 deletions packages/esm-active-visits-app/translations/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
"tests": "Tests",
"thereIsNoInformationToDisplayHere": "There is no information to display here",
"time": "Time",
"totalVisits": "Total Visits Today",
"visitStartTime": "Visit Time",
"visitSummary": "Visit Summary",
"visitType": "Visit Type"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import React from 'react';
import { Tile } from '@carbon/react';
import styles from './appointments-tile.scss';
import { useTranslation } from 'react-i18next';
import useAppointmentsData from './appointments-tile.resources';

const AppointmentsTile: React.FC = () => {
const { data: appointmentsData } = useAppointmentsData();

const { t } = useTranslation();

return (
<React.Fragment>
<Tile className={styles.tileContainer}>
<div>
<div className={styles.tileContent}>
<div className={styles.tileHeader}>
<header>{t('scheduledForToday', 'Scheduled For Today')}</header>
</div>
<div className={styles.displayDetails}>
<div className={styles.countLabel}>Patients</div>
<div className={styles.displayData}>{appointmentsData?.length ?? 0}</div>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same comments apply about cleaning this component

</div>
</div>
</div>
</Tile>
</React.Fragment>
);
};

export default AppointmentsTile;
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { openmrsFetch } from '@openmrs/esm-framework';
import useSWR from 'swr';
import dayjs from 'dayjs';

const useAppointmentsData = () => {
const omrsDateFormat = 'YYYY-MM-DDTHH:mm:ss.SSSZZ';
const appointmentDate = dayjs(new Date().setHours(0, 0, 0, 0)).format(omrsDateFormat);

const url = `ws/rest/v1/appointment/all?forDate=${appointmentDate}`;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
const url = `ws/rest/v1/appointment/all?forDate=${appointmentDate}`;
const url = `${restBaseUrl}/appointment/all?forDate=${appointmentDate}`;


const { data, error, isLoading } = useSWR<{ data: Array<any> }>(url, openmrsFetch);

const responseData = data?.data;

return { data: responseData, error, isLoading };
Copy link
Member

@pirupius pirupius Apr 16, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Was this intentional? Does data?.data have the necessary information to be displayed or do u have to chain results like the rest

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On the initial render data.data will throw an error because the network request hasn't been fulfilled yet.

};

export default useAppointmentsData;
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
@use '@carbon/type';
@use '@carbon/colors';
@import '~@openmrs/esm-styleguide/src/vars';

.tileContent {
display: flex;
flex-direction: column;
align-items: space-between;
}

.tileContainer {
background-color: white;
border: 0.0625rem solid #e0e0e0;
height: 7.875rem;
padding: 1rem;
margin: 0.5rem 0.5rem;
}

.tileHeader {
font-size: 0.875rem;
font-weight: 600;
line-height: 1.28572;
letter-spacing: 0.16px;
color: #525252;
}

.displayDetails {
margin-top: 1.5rem;
}

.displayData {
font-size: 1.75rem;
font-weight: 400;
line-height: 1.28572;
letter-spacing: 0;
color: #161616;
}

.countLabel {
@include type.type-style('label-01');
color: $text-02;
}
5 changes: 5 additions & 0 deletions packages/esm-appointments-app/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,4 +98,9 @@ export const patientAppointmentsCancelConfirmationDialog = getAsyncLifecycle(
options,
);

export const homeAppointmentsTile = getAsyncLifecycle(
() => import('./homepage-tile/appointments-tile.component'),
options,
);

export const appointmentsFormWorkspace = getAsyncLifecycle(() => import('./form/appointments-form.component'), options);
5 changes: 5 additions & 0 deletions packages/esm-appointments-app/src/routes.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,11 @@
"webservices.rest": "^2.2.0"
},
"extensions": [
{
"name": "home-appointments-tile",
"slot": "metrics-extension-slot",
"component": "homeAppointmentsTile"
},
{
"name": "home-appointments",
"slot": "homepage-widgets-slot",
Expand Down
1 change: 1 addition & 0 deletions packages/esm-appointments-app/translations/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,7 @@
"saveAndClose": "Save and close",
"scheduled": "Scheduled",
"scheduledAppointments": "Scheduled appointments",
"scheduledForToday": "Scheduled For Today",
"search": "Search",
"searchForAVisitType": "Search for a visit type",
"selectAppointmentStatus": "Select status",
Expand Down