@ampath/esm-dha-workflow-app 4.0.0-next.14 → 4.0.0-next.16
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/104.js +2 -0
- package/dist/104.js.LICENSE.txt +9 -0
- package/dist/104.js.map +1 -0
- package/dist/178.js +1 -0
- package/dist/178.js.map +1 -0
- package/dist/306.js +1 -1
- package/dist/306.js.map +1 -1
- package/dist/560.js +1 -0
- package/dist/560.js.map +1 -0
- package/dist/635.js +1 -0
- package/dist/635.js.map +1 -0
- package/dist/695.js +1 -0
- package/dist/695.js.map +1 -0
- package/dist/709.js +1 -0
- package/dist/709.js.map +1 -0
- package/dist/710.js +2 -0
- package/dist/{198.js.LICENSE.txt → 710.js.LICENSE.txt} +0 -10
- package/dist/710.js.map +1 -0
- package/dist/875.js +2 -0
- package/dist/875.js.map +1 -0
- package/dist/91.js +1 -1
- package/dist/91.js.map +1 -1
- package/dist/93.js +1 -1
- package/dist/978.js +1 -0
- package/dist/978.js.map +1 -0
- package/dist/esm-dha-workflow-app.js +1 -0
- package/dist/{openmrs-esm-home-app.js.buildmanifest.json → esm-dha-workflow-app.js.buildmanifest.json} +242 -73
- package/dist/esm-dha-workflow-app.js.map +1 -0
- package/dist/main.js +1 -1
- package/dist/main.js.map +1 -1
- package/dist/routes.json +1 -1
- package/package.json +2 -2
- package/src/config-schema.ts +115 -33
- package/src/consultation/action-button.component.tsx +34 -0
- package/src/consultation/action-overflow-menu-item.component.tsx +34 -0
- package/src/consultation/consultation-room.component.tsx +60 -0
- package/src/consultation/consultation.scss +5 -0
- package/src/consultation/consultation.tsx +9 -2
- package/src/hooks/useActions.ts +148 -0
- package/src/hooks/useQueueEntries.ts +35 -0
- package/src/index.ts +11 -1
- package/src/metrics/metrics-cards/attended-patients.extension.tsx +18 -0
- package/src/metrics/metrics-cards/metrics-card.component.tsx +86 -0
- package/src/metrics/metrics-cards/metrics-card.scss +106 -0
- package/src/metrics/metrics-cards/waiting-patients.extension.tsx +18 -0
- package/src/metrics/metrics-container.component.tsx +16 -0
- package/src/metrics/metrics-container.scss +36 -0
- package/src/metrics/metrics.resource.ts +101 -0
- package/src/modals/sign-off-modal.scss +7 -0
- package/src/modals/sign-off-modal.tsx +52 -0
- package/src/root.component.tsx +2 -1
- package/src/routes.json +18 -0
- package/src/service-queues/service-queues.resource.ts +62 -0
- package/src/triage/room/room.component.tsx +39 -0
- package/src/triage/room/room.scss +29 -0
- package/src/triage/triage.component.tsx +36 -0
- package/src/triage/triage.module.scss +19 -0
- package/src/triage/triage.resource.ts +19 -0
- package/src/triage/types.ts +22 -0
- package/src/types/types.ts +100 -0
- package/dist/198.js +0 -2
- package/dist/198.js.map +0 -1
- package/dist/200.js +0 -2
- package/dist/200.js.map +0 -1
- package/dist/860.js +0 -1
- package/dist/860.js.map +0 -1
- package/dist/openmrs-esm-home-app.js +0 -1
- package/dist/openmrs-esm-home-app.js.map +0 -1
- /package/dist/{200.js.LICENSE.txt → 875.js.LICENSE.txt} +0 -0
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { openmrsFetch, restBaseUrl } from "@openmrs/esm-framework";
|
|
2
|
+
import useSWR from 'swr';
|
|
3
|
+
import { useSWRConfig } from 'swr/_internal';
|
|
4
|
+
import { type QueueEntryResponse } from "../types/types";
|
|
5
|
+
import { useMemo } from "react";
|
|
6
|
+
|
|
7
|
+
export function useMutateQueueEntries() {
|
|
8
|
+
const { mutate } = useSWRConfig();
|
|
9
|
+
|
|
10
|
+
return {
|
|
11
|
+
mutateQueueEntries: () => {
|
|
12
|
+
return mutate((key) => {
|
|
13
|
+
return (
|
|
14
|
+
typeof key === 'string' &&
|
|
15
|
+
(key.includes(`${restBaseUrl}/queue-entry`) || key.includes(`${restBaseUrl}/visit-queue-entry`))
|
|
16
|
+
);
|
|
17
|
+
}).then(() => {
|
|
18
|
+
window.dispatchEvent(new CustomEvent('queue-entry-updated'));
|
|
19
|
+
});
|
|
20
|
+
},
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function useQueueEntries() {
|
|
25
|
+
const queueEntryBaseUrl = `${restBaseUrl}/queue-entry?` +
|
|
26
|
+
`isEnded=false&service=7f7ec7ad-cdd7-4ed9-bc2e-5c5bd9f065b2&location=18c343eb-b353-462a-9139-b16606e6b6c2`;
|
|
27
|
+
const { data, isValidating, isLoading, error: pageError } = useSWR<QueueEntryResponse, Error>(queueEntryBaseUrl, openmrsFetch);
|
|
28
|
+
|
|
29
|
+
const queueEntries = useMemo(() => data, [data]);
|
|
30
|
+
|
|
31
|
+
return {
|
|
32
|
+
queueEntries,
|
|
33
|
+
isLoading
|
|
34
|
+
}
|
|
35
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
import { getAsyncLifecycle, defineConfigSchema, getSyncLifecycle } from '@openmrs/esm-framework';
|
|
8
8
|
import { configSchema } from './config-schema';
|
|
9
9
|
|
|
10
|
-
const moduleName = '@ampath/esm-
|
|
10
|
+
const moduleName = '@ampath/openmrs-esm-home-app.js';
|
|
11
11
|
|
|
12
12
|
const options = {
|
|
13
13
|
featureName: 'Consulation Workflow',
|
|
@@ -44,8 +44,18 @@ export const navLinks = getAsyncLifecycle(() => import('./side-nav-menu/nav-link
|
|
|
44
44
|
*/
|
|
45
45
|
export const root = getAsyncLifecycle(() => import('./root.component'), options);
|
|
46
46
|
export const registry = getAsyncLifecycle(() => import('./registry/registry.component'), options);
|
|
47
|
+
export const waitingPatientsExtension = getAsyncLifecycle(
|
|
48
|
+
() => import('./metrics/metrics-cards/waiting-patients.extension'),
|
|
49
|
+
options,
|
|
50
|
+
);
|
|
51
|
+
export const attendedToPatientsExtension = getAsyncLifecycle(
|
|
52
|
+
() => import('./metrics/metrics-cards/attended-patients.extension'),
|
|
53
|
+
options,
|
|
54
|
+
);
|
|
47
55
|
|
|
48
56
|
export const workflowRegistryLink = getAsyncLifecycle(() => import('./widgets/workflow-registry-link.extension'), {
|
|
49
57
|
featureName: 'workflow-registry-link',
|
|
50
58
|
moduleName,
|
|
51
59
|
});
|
|
60
|
+
|
|
61
|
+
export const triage = getAsyncLifecycle(() => import('./triage/triage.component'), options);
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { useTranslation } from 'react-i18next';
|
|
3
|
+
import { MetricsCard, MetricsCardHeader, MetricsCardBody, MetricsCardItem } from './metrics-card.component';
|
|
4
|
+
import { useServiceMetricsCount } from '../metrics.resource';
|
|
5
|
+
|
|
6
|
+
export default function AttendedToPatientsExtension() {
|
|
7
|
+
const { t } = useTranslation();
|
|
8
|
+
const { serviceCount, isLoading } = useServiceMetricsCount("COMPLETED");
|
|
9
|
+
|
|
10
|
+
return (
|
|
11
|
+
<MetricsCard>
|
|
12
|
+
<MetricsCardHeader title={t('patientsAttendedTo', 'Patients attended to')} />
|
|
13
|
+
<MetricsCardBody>
|
|
14
|
+
<MetricsCardItem label={t('patients', 'Patients')} value={isLoading ? '--' : serviceCount ?? 0} />
|
|
15
|
+
</MetricsCardBody>
|
|
16
|
+
</MetricsCard>
|
|
17
|
+
);
|
|
18
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import classNames from 'classnames';
|
|
3
|
+
import { Layer, Tile } from '@carbon/react';
|
|
4
|
+
import { ArrowRight } from '@carbon/react/icons';
|
|
5
|
+
import { ConfigurableLink } from '@openmrs/esm-framework';
|
|
6
|
+
import styles from './metrics-card.scss';
|
|
7
|
+
|
|
8
|
+
interface MetricsCardProps {
|
|
9
|
+
children?: React.ReactNode;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export const MetricsCard: React.FC<MetricsCardProps> = ({ children }) => {
|
|
13
|
+
return (
|
|
14
|
+
<Layer
|
|
15
|
+
className={classNames({
|
|
16
|
+
cardWithChildren: children,
|
|
17
|
+
})}>
|
|
18
|
+
<Tile className={styles.tileContainer}>{children}</Tile>
|
|
19
|
+
</Layer>
|
|
20
|
+
);
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
interface MetricsCardHeaderProps {
|
|
24
|
+
title: string;
|
|
25
|
+
children?: React.ReactNode;
|
|
26
|
+
link?: string;
|
|
27
|
+
linkText?: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export const MetricsCardHeader: React.FC<MetricsCardHeaderProps> = ({ title, children, link, linkText }) => {
|
|
31
|
+
return (
|
|
32
|
+
<div className={styles.tileHeader}>
|
|
33
|
+
<div className={styles.headerLabelContainer}>
|
|
34
|
+
<label className={styles.headerLabel}>{title}</label>
|
|
35
|
+
{children}
|
|
36
|
+
</div>
|
|
37
|
+
{link && (
|
|
38
|
+
<div className={styles.link}>
|
|
39
|
+
<ConfigurableLink className={styles.link} to={link}>
|
|
40
|
+
{linkText}
|
|
41
|
+
</ConfigurableLink>
|
|
42
|
+
<ArrowRight size={16} />
|
|
43
|
+
</div>
|
|
44
|
+
)}
|
|
45
|
+
</div>
|
|
46
|
+
);
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
interface MetricsCardBodyProps {
|
|
50
|
+
children?: React.ReactNode;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export const MetricsCardBody: React.FC<MetricsCardBodyProps> = ({ children }) => {
|
|
54
|
+
return <div className={styles.metricsContainer}>{children}</div>;
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
interface MetricsCardItemProps {
|
|
58
|
+
label: string;
|
|
59
|
+
/** If the value is null, the item will not be rendered. */
|
|
60
|
+
value: number | string | null;
|
|
61
|
+
small?: boolean;
|
|
62
|
+
color?: 'default' | 'red';
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export const MetricsCardItem: React.FC<MetricsCardItemProps> = ({ label, value, small, color }) => {
|
|
66
|
+
if (value === null) {
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return (
|
|
71
|
+
<div
|
|
72
|
+
className={classNames(styles.metricItem, {
|
|
73
|
+
[styles.smallItem]: small,
|
|
74
|
+
[styles.mainItem]: !small,
|
|
75
|
+
})}>
|
|
76
|
+
<span className={styles.metricLabel}>{label}</span>
|
|
77
|
+
<p
|
|
78
|
+
className={classNames(styles.metricValue, {
|
|
79
|
+
[styles.red]: color === 'red',
|
|
80
|
+
[styles.smallValue]: small,
|
|
81
|
+
})}>
|
|
82
|
+
{value}
|
|
83
|
+
</p>
|
|
84
|
+
</div>
|
|
85
|
+
);
|
|
86
|
+
};
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
@use '@carbon/layout';
|
|
2
|
+
@use '@carbon/type';
|
|
3
|
+
@use '@carbon/colors';
|
|
4
|
+
@use '@openmrs/esm-styleguide/src/vars' as *;
|
|
5
|
+
|
|
6
|
+
.tileContainer {
|
|
7
|
+
border: 1px solid $ui-03;
|
|
8
|
+
min-height: 7.875rem;
|
|
9
|
+
height: 100%;
|
|
10
|
+
padding: layout.$spacing-05;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
.tileHeader {
|
|
14
|
+
display: flex;
|
|
15
|
+
justify-content: space-between;
|
|
16
|
+
align-items: baseline;
|
|
17
|
+
margin-bottom: layout.$spacing-03;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
.headerLabelContainer {
|
|
21
|
+
display: flex;
|
|
22
|
+
height: layout.$spacing-07;
|
|
23
|
+
align-items: center;
|
|
24
|
+
|
|
25
|
+
:global(.cds--dropdown__wrapper--inline) {
|
|
26
|
+
gap: 0;
|
|
27
|
+
|
|
28
|
+
label {
|
|
29
|
+
@include type.type-style('heading-compact-01');
|
|
30
|
+
color: $text-02;
|
|
31
|
+
margin-right: layout.$spacing-03;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
:global(.cds--list-box__menu-icon) {
|
|
36
|
+
height: layout.$spacing-05;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
.link {
|
|
41
|
+
text-decoration: none;
|
|
42
|
+
display: flex;
|
|
43
|
+
align-items: center;
|
|
44
|
+
color: $interactive-01;
|
|
45
|
+
|
|
46
|
+
svg {
|
|
47
|
+
margin-left: layout.$spacing-03;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
.headerLabel {
|
|
52
|
+
@include type.type-style('heading-compact-01');
|
|
53
|
+
color: $text-02;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
.metricsContainer {
|
|
57
|
+
display: flex;
|
|
58
|
+
gap: layout.$spacing-07;
|
|
59
|
+
align-items: stretch;
|
|
60
|
+
flex-wrap: wrap;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
.metricItem {
|
|
64
|
+
display: inline-block;
|
|
65
|
+
flex-direction: column;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
.metricLabel {
|
|
69
|
+
@include type.type-style('label-01');
|
|
70
|
+
color: $text-02;
|
|
71
|
+
margin-bottom: layout.$spacing-02;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
.metricValue {
|
|
75
|
+
@include type.type-style('heading-04');
|
|
76
|
+
color: $ui-05;
|
|
77
|
+
margin: 0;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
.mainItem {
|
|
81
|
+
margin-right: layout.$spacing-07;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
.smallItem {
|
|
85
|
+
flex-direction: row;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
.smallValue {
|
|
89
|
+
font-size: 1rem !important;
|
|
90
|
+
margin-top: layout.$spacing-02;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
.red {
|
|
94
|
+
color: colors.$red-50;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Overriding styles for RTL support
|
|
98
|
+
html[dir='rtl'] {
|
|
99
|
+
.link {
|
|
100
|
+
svg {
|
|
101
|
+
margin-right: layout.$spacing-03;
|
|
102
|
+
margin-left: unset;
|
|
103
|
+
transform: scale(-1, 1);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { useTranslation } from 'react-i18next';
|
|
3
|
+
import { MetricsCard, MetricsCardHeader, MetricsCardBody, MetricsCardItem } from './metrics-card.component';
|
|
4
|
+
import { useServiceMetricsCount } from '../metrics.resource';
|
|
5
|
+
|
|
6
|
+
export default function WaitingPatientsExtension() {
|
|
7
|
+
const { t } = useTranslation();
|
|
8
|
+
const { serviceCount, isLoading } = useServiceMetricsCount("WAITING");
|
|
9
|
+
|
|
10
|
+
return (
|
|
11
|
+
<MetricsCard>
|
|
12
|
+
<MetricsCardHeader title={t('patientsInWaiting', 'Patients in waiting')} />
|
|
13
|
+
<MetricsCardBody>
|
|
14
|
+
<MetricsCardItem label={t('patients', 'Patients')} value={isLoading ? '--' : serviceCount ?? 0} />
|
|
15
|
+
</MetricsCardBody>
|
|
16
|
+
</MetricsCard>
|
|
17
|
+
);
|
|
18
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { ExtensionSlot } from '@openmrs/esm-framework';
|
|
3
|
+
import styles from './metrics-container.scss';
|
|
4
|
+
|
|
5
|
+
export interface Service {
|
|
6
|
+
display: string;
|
|
7
|
+
uuid?: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function MetricsContainer() {
|
|
11
|
+
return (
|
|
12
|
+
<ExtensionSlot name="clinic-metrics-slot" className={styles.cardContainer} data-testid="clinic-metrics" />
|
|
13
|
+
);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export default MetricsContainer;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
@use '@carbon/layout';
|
|
2
|
+
@use '@openmrs/esm-styleguide/src/vars' as *;
|
|
3
|
+
|
|
4
|
+
.cardContainer {
|
|
5
|
+
background-color: $ui-02;
|
|
6
|
+
display: flex;
|
|
7
|
+
padding: layout.$spacing-05;
|
|
8
|
+
flex-flow: row wrap;
|
|
9
|
+
gap: layout.$spacing-05;
|
|
10
|
+
align-items: stretch;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
.cardContainer > * {
|
|
14
|
+
flex: 1 0 0%;
|
|
15
|
+
min-width: 16rem;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// If we're on tablet and the screen is too small for 3 cards across
|
|
19
|
+
// @TODO: This will do nonsense things if there are not exactly 3 cards
|
|
20
|
+
@media (max-width: calc(layout.$spacing-05 * 4 + 18.75rem * 3)) {
|
|
21
|
+
:global(.omrs-breakpoint-lt-desktop) {
|
|
22
|
+
.cardContainer > *:has(:global(.cardWithChildren)) {
|
|
23
|
+
order: 999;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// If we're on desktop and the screen is too small for the left nav bar
|
|
29
|
+
// plus 3 cards across
|
|
30
|
+
@media (max-width: calc(16rem + layout.$spacing-05 * 4 + 18.75rem * 3)) {
|
|
31
|
+
:global(.omrs-breakpoint-gt-tablet) {
|
|
32
|
+
.cardContainer > *:has(:global(.cardWithChildren)) {
|
|
33
|
+
order: 999;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { useSession, type Visit, openmrsFetch, restBaseUrl } from '@openmrs/esm-framework';
|
|
2
|
+
import dayjs from 'dayjs';
|
|
3
|
+
import useSWR from 'swr';
|
|
4
|
+
|
|
5
|
+
// export function useActiveVisits() {
|
|
6
|
+
// const currentUserSession = useSession();
|
|
7
|
+
// const startDate = dayjs().format('YYYY-MM-DD');
|
|
8
|
+
// const sessionLocation = currentUserSession?.sessionLocation?.uuid;
|
|
9
|
+
|
|
10
|
+
// const customRepresentation =
|
|
11
|
+
// 'custom:(uuid,patient:(uuid,identifiers:(identifier,uuid),person:(age,display,gender,uuid)),' +
|
|
12
|
+
// 'visitType:(uuid,name,display),location:(uuid,name,display),startDatetime,' +
|
|
13
|
+
// 'stopDatetime)&fromStartDate=' +
|
|
14
|
+
// startDate +
|
|
15
|
+
// '&location=' +
|
|
16
|
+
// sessionLocation;
|
|
17
|
+
// const url = `${restBaseUrl}/visit?includeInactive=false&v=${customRepresentation}`;
|
|
18
|
+
// const { data, error, isLoading, isValidating } = useSWR<{ data: { results: Array<Visit> } }, Error>(
|
|
19
|
+
// sessionLocation ? url : null,
|
|
20
|
+
// openmrsFetch,
|
|
21
|
+
// );
|
|
22
|
+
|
|
23
|
+
// // Create a Set to store unique patient UUIDs
|
|
24
|
+
// const uniquePatientUUIDs = new Set();
|
|
25
|
+
|
|
26
|
+
// data?.data?.results.forEach((visit) => {
|
|
27
|
+
// const patientUUID = visit.patient?.uuid;
|
|
28
|
+
// const isToday = dayjs(visit.startDatetime).isToday();
|
|
29
|
+
// if (patientUUID && isToday) {
|
|
30
|
+
// uniquePatientUUIDs.add(patientUUID);
|
|
31
|
+
// }
|
|
32
|
+
// });
|
|
33
|
+
|
|
34
|
+
// return {
|
|
35
|
+
// activeVisitsCount: uniquePatientUUIDs.size,
|
|
36
|
+
// isLoading,
|
|
37
|
+
// error,
|
|
38
|
+
// isValidating,
|
|
39
|
+
// };
|
|
40
|
+
// }
|
|
41
|
+
|
|
42
|
+
// Statuses: Waiting, Finished Service, In Service
|
|
43
|
+
export function useServiceMetricsCount(status: string = 'Waiting', service: string = "7f7ec7ad-cdd7-4ed9-bc2e-5c5bd9f065b2") {
|
|
44
|
+
const currentUserSession = useSession();
|
|
45
|
+
const location = currentUserSession?.sessionLocation?.uuid;
|
|
46
|
+
|
|
47
|
+
const apiUrl =
|
|
48
|
+
`${restBaseUrl}/queue-entry-metrics?status=${status}&isEnded=false` +
|
|
49
|
+
(service ? `&service=${service}` : '') +
|
|
50
|
+
(location ? `&location=${location}` : '');
|
|
51
|
+
|
|
52
|
+
const { data, isLoading } = useSWR<
|
|
53
|
+
{
|
|
54
|
+
data: {
|
|
55
|
+
count: number;
|
|
56
|
+
};
|
|
57
|
+
},
|
|
58
|
+
Error
|
|
59
|
+
>(service ? apiUrl : null, openmrsFetch);
|
|
60
|
+
|
|
61
|
+
return {
|
|
62
|
+
serviceCount: data ? data?.data?.count : 0,
|
|
63
|
+
isLoading
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function useConsultationQueues(service: string = "7f7ec7ad-cdd7-4ed9-bc2e-5c5bd9f065b2", status: string = 'WAITING') {
|
|
68
|
+
const currentUserSession = useSession();
|
|
69
|
+
const location = currentUserSession?.sessionLocation?.uuid;
|
|
70
|
+
|
|
71
|
+
const customRepresentation =
|
|
72
|
+
'custom:(uuid,display,queue,status,patient:(uuid,display,person,identifiers:(uuid,display,identifier,identifierType)),' +
|
|
73
|
+
'visit:(uuid,display,startDatetime))'
|
|
74
|
+
|
|
75
|
+
const apiUrl =
|
|
76
|
+
`${restBaseUrl}/queue-entry?status=${status}&isEnded=false` +
|
|
77
|
+
(service ? `&service=${service}` : '') +
|
|
78
|
+
(location ? `&location=${location}` : '') +
|
|
79
|
+
`&v=${customRepresentation}`;
|
|
80
|
+
|
|
81
|
+
const { data, isLoading } = useSWR<
|
|
82
|
+
{
|
|
83
|
+
data: {
|
|
84
|
+
results: Array<{
|
|
85
|
+
patient: {
|
|
86
|
+
uuid: string;
|
|
87
|
+
display: string;
|
|
88
|
+
},
|
|
89
|
+
visit: {
|
|
90
|
+
uuid: string;
|
|
91
|
+
display: string;
|
|
92
|
+
startDatetime: string;
|
|
93
|
+
}
|
|
94
|
+
}>;
|
|
95
|
+
};
|
|
96
|
+
},
|
|
97
|
+
Error
|
|
98
|
+
>(service ? apiUrl : null, openmrsFetch);
|
|
99
|
+
|
|
100
|
+
return { data, isLoading };
|
|
101
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import React, { useCallback, useState } from 'react';
|
|
2
|
+
import { Button, ButtonSkeleton, ModalBody, ModalFooter, ModalHeader } from '@carbon/react';
|
|
3
|
+
import { useTranslation } from 'react-i18next';
|
|
4
|
+
import styles from './sign-off-modal.scss';
|
|
5
|
+
import { type QueueEntry } from '../types/types';
|
|
6
|
+
|
|
7
|
+
interface SignOffModalProps {
|
|
8
|
+
queueEntries: Array<QueueEntry>;
|
|
9
|
+
closeModal: () => void;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const SignOffModal: React.FC<SignOffModalProps> = ({ queueEntries, closeModal }) => {
|
|
13
|
+
const { t } = useTranslation();
|
|
14
|
+
const [isSubmitting, setIsSubmitting] = useState(false);
|
|
15
|
+
|
|
16
|
+
const handleSignOffRequest = useCallback(() => {
|
|
17
|
+
setIsSubmitting(true);
|
|
18
|
+
}, []);
|
|
19
|
+
|
|
20
|
+
return (
|
|
21
|
+
<div>
|
|
22
|
+
<ModalHeader
|
|
23
|
+
closeModal={closeModal}
|
|
24
|
+
label={t('signOffMessage', 'Sign off message')}
|
|
25
|
+
title={t('signOffMessage', 'Sign off message')}
|
|
26
|
+
/>
|
|
27
|
+
<ModalBody>
|
|
28
|
+
<p className={styles.subHeading} id="subHeading">
|
|
29
|
+
{t(
|
|
30
|
+
'signOffMessage',
|
|
31
|
+
'Sign off message',
|
|
32
|
+
)}
|
|
33
|
+
.
|
|
34
|
+
</p>
|
|
35
|
+
</ModalBody>
|
|
36
|
+
<ModalFooter>
|
|
37
|
+
<Button kind="secondary" onClick={closeModal}>
|
|
38
|
+
{t('cancel', 'Cancel')}
|
|
39
|
+
</Button>
|
|
40
|
+
{isSubmitting === true ? (
|
|
41
|
+
<ButtonSkeleton />
|
|
42
|
+
) : (
|
|
43
|
+
<Button kind="danger" onClick={handleSignOffRequest}>
|
|
44
|
+
{t('signOff', 'SignOff')}
|
|
45
|
+
</Button>
|
|
46
|
+
)}
|
|
47
|
+
</ModalFooter>
|
|
48
|
+
</div>
|
|
49
|
+
);
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
export default SignOffModal;
|
package/src/root.component.tsx
CHANGED
|
@@ -5,6 +5,7 @@ import { BrowserRouter, Route, Routes } from 'react-router-dom';
|
|
|
5
5
|
import RegistryComponent from './registry/registry.component';
|
|
6
6
|
import LeftPanel from './left-panel/left-panel.component';
|
|
7
7
|
import Consultation from './consultation/consultation';
|
|
8
|
+
import Triage from './triage/triage.component';
|
|
8
9
|
|
|
9
10
|
const Root: React.FC = () => {
|
|
10
11
|
return (
|
|
@@ -14,9 +15,9 @@ const Root: React.FC = () => {
|
|
|
14
15
|
<Routes>
|
|
15
16
|
<Route path="" element={<RegistryComponent />} />
|
|
16
17
|
<Route path="registry" element={<RegistryComponent />} />
|
|
17
|
-
<Route path="triage" element={<Consultation />} />
|
|
18
18
|
<Route path="consultation" element={<Consultation />} />
|
|
19
19
|
<Route path="*" element={<RegistryComponent />} />
|
|
20
|
+
<Route path="triage" element={<Triage />} />
|
|
20
21
|
</Routes>
|
|
21
22
|
</main>
|
|
22
23
|
<WorkspaceContainer contextKey="home" />
|
package/src/routes.json
CHANGED
|
@@ -23,6 +23,24 @@
|
|
|
23
23
|
"slot": "dha-workflow-slot",
|
|
24
24
|
"online": true,
|
|
25
25
|
"offline": true
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
"name": "metrics-card-patients-in-waiting",
|
|
29
|
+
"component": "waitingPatientsExtension",
|
|
30
|
+
"slot": "clinic-metrics-slot",
|
|
31
|
+
"order": 1
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
"name": "metrics-card-patients-attended-to",
|
|
35
|
+
"component": "attendedToPatientsExtension",
|
|
36
|
+
"slot": "clinic-metrics-slot",
|
|
37
|
+
"order": 2
|
|
38
|
+
}
|
|
39
|
+
],
|
|
40
|
+
"modals": [
|
|
41
|
+
{
|
|
42
|
+
"name": "sign-off-queue-entry-modal",
|
|
43
|
+
"component": "signOffModal"
|
|
26
44
|
}
|
|
27
45
|
]
|
|
28
46
|
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { type Encounter, formatDate, openmrsFetch, parseDate, restBaseUrl } from "@openmrs/esm-framework";
|
|
2
|
+
import { type Identifer, type MappedEncounter, type MappedVisitQueueEntry, type QueueEntry } from "../types/types";
|
|
3
|
+
import dayjs from "dayjs";
|
|
4
|
+
|
|
5
|
+
export function serveQueueEntry(servicePointName: string, ticketNumber: string, status: string) {
|
|
6
|
+
const abortController = new AbortController();
|
|
7
|
+
|
|
8
|
+
return openmrsFetch(`${restBaseUrl}/queueutil/assignticket`, {
|
|
9
|
+
method: 'POST',
|
|
10
|
+
headers: {
|
|
11
|
+
'Content-Type': 'application/json',
|
|
12
|
+
},
|
|
13
|
+
signal: abortController.signal,
|
|
14
|
+
body: {
|
|
15
|
+
servicePointName,
|
|
16
|
+
ticketNumber,
|
|
17
|
+
status,
|
|
18
|
+
},
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const mapEncounterProperties = (encounter: Encounter): MappedEncounter => ({
|
|
23
|
+
diagnoses: encounter.diagnoses,
|
|
24
|
+
encounterDatetime: encounter.encounterDatetime,
|
|
25
|
+
encounterType: encounter.encounterType.display,
|
|
26
|
+
obs: encounter.obs,
|
|
27
|
+
provider: encounter.encounterProviders[0]?.provider?.person?.display,
|
|
28
|
+
uuid: encounter.uuid,
|
|
29
|
+
voided: encounter.voided,
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
export const mapVisitQueueEntryProperties = (
|
|
33
|
+
queueEntry: QueueEntry,
|
|
34
|
+
visitQueueNumberAttributeUuid: string,
|
|
35
|
+
): MappedVisitQueueEntry => ({
|
|
36
|
+
id: queueEntry.uuid,
|
|
37
|
+
encounters: queueEntry.visit?.encounters?.map(mapEncounterProperties),
|
|
38
|
+
name: queueEntry.display,
|
|
39
|
+
patientUuid: queueEntry.patient.uuid,
|
|
40
|
+
patientAge: queueEntry.patient.person?.age + '',
|
|
41
|
+
patientDob: queueEntry?.patient?.person?.birthdate
|
|
42
|
+
? formatDate(parseDate(queueEntry.patient.person.birthdate), { time: false })
|
|
43
|
+
: '--',
|
|
44
|
+
patientGender: queueEntry.patient.person.gender,
|
|
45
|
+
queue: queueEntry.queue,
|
|
46
|
+
priority: queueEntry.priority,
|
|
47
|
+
priorityComment: queueEntry.priorityComment,
|
|
48
|
+
status: queueEntry.status,
|
|
49
|
+
startedAt: dayjs(queueEntry.startedAt).toDate(),
|
|
50
|
+
endedAt: queueEntry.endedAt ? dayjs(queueEntry.endedAt).toDate() : null,
|
|
51
|
+
visitType: queueEntry.visit?.visitType?.display,
|
|
52
|
+
queueLocation: (queueEntry?.queue as any)?.location?.uuid,
|
|
53
|
+
visitTypeUuid: queueEntry.visit?.visitType?.uuid,
|
|
54
|
+
visitUuid: queueEntry.visit?.uuid,
|
|
55
|
+
queueUuid: queueEntry.queue.uuid,
|
|
56
|
+
queueEntryUuid: queueEntry.uuid,
|
|
57
|
+
sortWeight: queueEntry.sortWeight,
|
|
58
|
+
visitQueueNumber: queueEntry.visit?.attributes?.find((e) => e?.attributeType?.uuid === visitQueueNumberAttributeUuid)
|
|
59
|
+
?.value,
|
|
60
|
+
identifiers: queueEntry.patient?.identifiers as Identifer[],
|
|
61
|
+
queueComingFrom: queueEntry?.queueComingFrom?.name,
|
|
62
|
+
});
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { navigate } from '@openmrs/esm-framework';
|
|
3
|
+
import { Button, Tile } from '@carbon/react';
|
|
4
|
+
import { type Patient } from '../types';
|
|
5
|
+
import styles from './room.scss';
|
|
6
|
+
|
|
7
|
+
interface RoomProps {
|
|
8
|
+
locationUuid?: string;
|
|
9
|
+
name: string;
|
|
10
|
+
patients: Array<Patient>;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const Room: React.FC<RoomProps> = ({ name, locationUuid, patients }) => {
|
|
14
|
+
function openPatientChart(patientUuid: string) {
|
|
15
|
+
navigate({
|
|
16
|
+
to: `patient/${patientUuid}/chart`,
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
return (
|
|
20
|
+
<div className={styles.room}>
|
|
21
|
+
<h4 className={styles.roomName}>{name}</h4>
|
|
22
|
+
|
|
23
|
+
<div className={styles.container}>
|
|
24
|
+
{patients.map((patient) => (
|
|
25
|
+
<Tile key={patient.uuid}>
|
|
26
|
+
<h5>{patient.display}</h5>
|
|
27
|
+
<p className={styles.field}>Age: {patient.patient.person.age}</p>
|
|
28
|
+
<p className={styles.field}>Status: {patient.status.display}</p>
|
|
29
|
+
<Button onClick={() => openPatientChart(patient.patient.uuid)} size="sm" className={styles.button}>
|
|
30
|
+
Start
|
|
31
|
+
</Button>
|
|
32
|
+
</Tile>
|
|
33
|
+
))}
|
|
34
|
+
</div>
|
|
35
|
+
</div>
|
|
36
|
+
);
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
export default Room;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
.container {
|
|
2
|
+
border: 1px solid #9e9e9e;
|
|
3
|
+
padding: 1rem;
|
|
4
|
+
display: flex;
|
|
5
|
+
flex-direction: column;
|
|
6
|
+
gap: 0.5rem;
|
|
7
|
+
width: fit-content;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
.room {
|
|
11
|
+
display: flex;
|
|
12
|
+
flex-direction: column;
|
|
13
|
+
align-items: flex-start;
|
|
14
|
+
margin: 1rem;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
.roomName {
|
|
18
|
+
margin-bottom: 0.5rem;
|
|
19
|
+
font-size: large;
|
|
20
|
+
font-weight: bold;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
.field {
|
|
24
|
+
margin: 0.25rem 0 0;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
.button {
|
|
28
|
+
margin-top: 0.5rem;
|
|
29
|
+
}
|