@openmrs/esm-active-visits-app 3.0.0-pre.93 → 3.0.1

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.
Files changed (39) hide show
  1. package/dist/160.openmrs-esm-active-visits-app.js +2 -0
  2. package/dist/160.openmrs-esm-active-visits-app.js.map +1 -0
  3. package/dist/165.openmrs-esm-active-visits-app.js +2 -0
  4. package/dist/165.openmrs-esm-active-visits-app.js.map +1 -0
  5. package/dist/319.openmrs-esm-active-visits-app.js +1 -0
  6. package/dist/371.openmrs-esm-active-visits-app.js +3 -0
  7. package/dist/371.openmrs-esm-active-visits-app.js.LICENSE.txt +29 -0
  8. package/dist/371.openmrs-esm-active-visits-app.js.map +1 -0
  9. package/dist/415.openmrs-esm-active-visits-app.js +2 -0
  10. package/dist/415.openmrs-esm-active-visits-app.js.map +1 -0
  11. package/dist/574.openmrs-esm-active-visits-app.js +1 -0
  12. package/dist/833.openmrs-esm-active-visits-app.js +2 -0
  13. package/dist/833.openmrs-esm-active-visits-app.js.map +1 -0
  14. package/dist/openmrs-esm-active-visits-app.js +1 -1
  15. package/dist/openmrs-esm-active-visits-app.js.buildmanifest.json +100 -25
  16. package/dist/openmrs-esm-active-visits-app.js.map +1 -1
  17. package/package.json +2 -2
  18. package/src/active-visits-widget/active-visits.component.tsx +188 -151
  19. package/src/active-visits-widget/active-visits.resource.tsx +37 -17
  20. package/src/active-visits-widget/active-visits.scss +34 -0
  21. package/src/index.ts +5 -0
  22. package/src/root.scss +16 -0
  23. package/src/visits-summary/visit-detail-overview.scss +195 -0
  24. package/src/visits-summary/visit-detail.component.tsx +75 -0
  25. package/src/visits-summary/visit.resource.ts +179 -0
  26. package/src/visits-summary/visits-components/encounter-list.component.tsx +125 -0
  27. package/src/visits-summary/visits-components/encounter-observations.component.tsx +42 -0
  28. package/src/visits-summary/visits-components/medications-summary.component.tsx +59 -0
  29. package/src/visits-summary/visits-components/notes-summary.component.tsx +31 -0
  30. package/src/visits-summary/visits-components/tests-summary.component.tsx +21 -0
  31. package/src/visits-summary/visits-components/visit-summary.component.tsx +124 -0
  32. package/translations/en.json +27 -1
  33. package/dist/237.openmrs-esm-active-visits-app.js +0 -2
  34. package/dist/237.openmrs-esm-active-visits-app.js.map +0 -1
  35. package/dist/50.openmrs-esm-active-visits-app.js +0 -1
  36. package/dist/678.openmrs-esm-active-visits-app.js +0 -3
  37. package/dist/678.openmrs-esm-active-visits-app.js.LICENSE.txt +0 -44
  38. package/dist/678.openmrs-esm-active-visits-app.js.map +0 -1
  39. package/dist/912.openmrs-esm-active-visits-app.js +0 -1
@@ -1,7 +1,12 @@
1
1
  import React, { useMemo, useEffect, useState, useCallback } from 'react';
2
- import DataTable, {
3
- TableContainer,
2
+ import {
3
+ DataTable,
4
+ DataTableSkeleton,
5
+ InlineLoading,
6
+ Pagination,
7
+ Search,
4
8
  Table,
9
+ TableContainer,
5
10
  TableHead,
6
11
  TableRow,
7
12
  TableHeader,
@@ -9,77 +14,78 @@ import DataTable, {
9
14
  TableCell,
10
15
  TableToolbar,
11
16
  TableToolbarContent,
12
- } from 'carbon-components-react/es/components/DataTable';
13
- import DataTableSkeleton from 'carbon-components-react/es/components/DataTableSkeleton';
14
- import Pagination from 'carbon-components-react/es/components/Pagination';
15
- import Search from 'carbon-components-react/es/components/Search';
16
- import { useLayoutType, useConfig, usePagination, ConfigurableLink } from '@openmrs/esm-framework';
17
- import { ActiveVisitRow, fetchActiveVisits } from './active-visits.resource';
18
- import styles from './active-visits.scss';
17
+ TableExpandRow,
18
+ TableExpandHeader,
19
+ } from 'carbon-components-react';
20
+ import { useLayoutType, useConfig, usePagination, ConfigurableLink, ExtensionSlot } from '@openmrs/esm-framework';
19
21
  import { useTranslation } from 'react-i18next';
22
+ import { ActiveVisit, useActiveVisits } from './active-visits.resource';
23
+ import styles from './active-visits.scss';
20
24
  import dayjs from 'dayjs';
21
25
 
22
- const headerData = [
23
- {
24
- id: 0,
25
- header: 'Visit Time',
26
- key: 'visitStartTime',
27
- },
28
- {
29
- id: 1,
30
- header: 'ID Number',
31
- key: 'IDNumber',
32
- },
33
- {
34
- id: 2,
35
- header: 'Name',
36
- key: 'name',
37
- },
38
- {
39
- id: 3,
40
- header: 'Gender',
41
- key: 'gender',
42
- },
43
- {
44
- id: 4,
45
- header: 'Age',
46
- key: 'age',
47
- },
48
- {
49
- id: 5,
50
- header: 'Visit Type',
51
- key: 'visitType',
52
- },
53
- ];
54
-
55
- function formatDatetime(startDatetime) {
56
- const todayDate = dayjs();
57
- const today =
58
- dayjs(startDatetime).get('date') === todayDate.get('date') &&
59
- dayjs(startDatetime).get('month') === todayDate.get('month') &&
60
- dayjs(startDatetime).get('year') === todayDate.get('year');
61
- if (today) {
62
- return `Today - ${dayjs(startDatetime).format('HH:mm')}`;
63
- } else {
64
- return dayjs(startDatetime).format("DD MMM 'YY - HH:mm");
65
- }
26
+ interface PaginationData {
27
+ goTo: (page: number) => void;
28
+ results: Array<ActiveVisit>;
29
+ currentPage: number;
66
30
  }
67
31
 
68
- const ActiveVisitsTable = (props) => {
32
+ const ActiveVisitsTable = () => {
69
33
  const { t } = useTranslation();
34
+ const config = useConfig();
70
35
  const layout = useLayoutType();
36
+
37
+ const { data: activeVisits, isError, isLoading, isValidating } = useActiveVisits();
38
+
71
39
  const desktopView = layout === 'desktop';
72
- const config = useConfig();
73
- const [currentPageSize, setPageSize] = useState(config?.activeVisits?.pageSize ?? 10);
74
40
  const pageSizes = config?.activeVisits?.pageSizes ?? [10, 20, 50];
75
- const [loading, setLoading] = useState(true);
76
- const [activeVisits, setActiveVisits] = useState<ActiveVisitRow[]>([]);
41
+ const [currentPageSize, setPageSize] = useState(config?.activeVisits?.pageSize ?? 10);
77
42
  const [searchString, setSearchString] = useState('');
78
43
 
44
+ const headerData = useMemo(
45
+ () => [
46
+ {
47
+ id: 0,
48
+ header: t('visitStartTime', 'Visit Time'),
49
+ key: 'visitStartTime',
50
+ },
51
+ {
52
+ id: 1,
53
+ header: t('idNumber', 'ID Number'),
54
+ key: 'idNumber',
55
+ },
56
+ {
57
+ id: 2,
58
+ header: t('name', 'Name'),
59
+ key: 'name',
60
+ },
61
+ {
62
+ id: 3,
63
+ header: t('gender', 'Gender'),
64
+ key: 'gender',
65
+ },
66
+ {
67
+ id: 4,
68
+ header: t('age', 'Age'),
69
+ key: 'age',
70
+ },
71
+ {
72
+ id: 5,
73
+ header: t('visitType', 'Visit Type'),
74
+ key: 'visitType',
75
+ },
76
+ ],
77
+ [t],
78
+ );
79
+
80
+ const rowData = activeVisits.map((visit) => ({
81
+ ...visit,
82
+ visitStartTime: formatDatetime(visit.visitStartTime),
83
+ }));
84
+
79
85
  const searchResults = useMemo(() => {
80
86
  if (searchString && searchString.trim() !== '') {
81
87
  const search = searchString.toLowerCase();
82
- return activeVisits.filter((activeVisitRow) =>
88
+ return rowData.filter((activeVisitRow) =>
83
89
  Object.keys(activeVisitRow).some((header) => {
84
90
  if (header === 'patientUuid') {
85
91
  return false;
@@ -88,105 +94,136 @@ const ActiveVisitsTable = (props) => {
88
94
  }),
89
95
  );
90
96
  } else {
91
- return activeVisits;
97
+ return rowData;
92
98
  }
93
- }, [searchString, activeVisits]);
94
- const { goTo, currentPage, results } = usePagination(searchResults, currentPageSize);
99
+ }, [searchString, rowData]);
95
100
 
96
- useEffect(() => {
97
- const activeVisits = fetchActiveVisits().subscribe((data) => {
98
- const rowData = data.results.map((visit, ind) => ({
99
- id: `${ind}`,
100
- visitStartTime: formatDatetime(visit.startDatetime),
101
- IDNumber: visit?.patient?.identifiers[0]?.identifier,
102
- name: visit?.patient?.person?.display,
103
- gender: visit?.patient?.person?.gender,
104
- age: visit?.patient?.person?.age,
105
- visitType: visit?.visitType.display,
106
- patientUuid: visit?.patient?.uuid,
107
- }));
108
- setActiveVisits(rowData);
109
- setLoading(false);
110
- });
111
- return () => activeVisits.unsubscribe();
112
- }, []);
101
+ const {
102
+ goTo,
103
+ results: paginatedActiveVisits,
104
+ currentPage,
105
+ }: PaginationData = usePagination(searchResults, currentPageSize);
113
106
 
114
107
  const handleSearch = useCallback((e) => setSearchString(e.target.value), []);
115
108
 
116
- return !loading ? (
117
- <div className={styles.activeVisitsContainer}>
118
- <div className={styles.activeVisitsDetailHeaderContainer}>
119
- <h4 className={styles.productiveHeading02}>{t('activeVisits', 'Active Visits')}</h4>
120
- </div>
121
- <DataTable rows={results} headers={headerData} isSortable>
122
- {({ rows, headers, getHeaderProps, getTableProps, getBatchActionProps }) => (
123
- <TableContainer title="" className={styles.tableContainer}>
124
- <TableToolbar>
125
- <TableToolbarContent>
126
- <Search
127
- tabIndex={getBatchActionProps().shouldShowBatchActions ? -1 : 0}
128
- placeholder="Filter table"
129
- onChange={handleSearch}
130
- />
131
- </TableToolbarContent>
132
- </TableToolbar>
133
- <Table {...getTableProps()} useZebraStyles>
134
- <TableHead>
135
- <TableRow style={{ height: desktopView ? '2rem' : '3rem' }}>
136
- {headers.map((header) => (
137
- <TableHeader {...getHeaderProps({ header })}>{header.header}</TableHeader>
138
- ))}
139
- </TableRow>
140
- </TableHead>
141
- <TableBody>
142
- {rows.map((row, ind) => (
143
- <TableRow key={row.id} style={{ height: desktopView ? '2rem' : '3rem' }}>
144
- {row.cells.map((cell) => (
145
- <TableCell key={cell.id}>
146
- {cell.info.header === 'name' ? (
147
- <ConfigurableLink to={`\${openmrsSpaBase}/patient/${results[ind]?.patientUuid}/chart/`}>
148
- {cell.value}
149
- </ConfigurableLink>
150
- ) : (
151
- cell.value
152
- )}
153
- </TableCell>
109
+ useEffect(() => {
110
+ if (currentPage !== 1) {
111
+ goTo(1);
112
+ }
113
+ }, [searchString]);
114
+
115
+ if (isLoading) {
116
+ return <DataTableSkeleton role="progressbar" />;
117
+ }
118
+ if (activeVisits?.length) {
119
+ return (
120
+ <div className={styles.activeVisitsContainer}>
121
+ <div className={styles.activeVisitsDetailHeaderContainer}>
122
+ <h4 className={styles.productiveHeading02}>{t('activeVisits', 'Active Visits')}</h4>
123
+ <div className={styles.backgroundDataFetchingIndicator}>
124
+ <span>{isValidating ? <InlineLoading /> : null}</span>
125
+ </div>
126
+ </div>
127
+ <DataTable rows={paginatedActiveVisits} headers={headerData} isSortable>
128
+ {({ rows, headers, getHeaderProps, getTableProps, getBatchActionProps, getRowProps }) => (
129
+ <TableContainer title="" className={styles.tableContainer}>
130
+ <TableToolbar>
131
+ <TableToolbarContent>
132
+ <Search
133
+ tabIndex={getBatchActionProps().shouldShowBatchActions ? -1 : 0}
134
+ labelText=""
135
+ placeholder={t('filterTable', 'Filter table')}
136
+ onChange={handleSearch}
137
+ />
138
+ </TableToolbarContent>
139
+ </TableToolbar>
140
+ <Table className={styles.activeVisitsTable} {...getTableProps()} size={desktopView ? 'short' : 'normal'}>
141
+ <TableHead>
142
+ <TableRow>
143
+ <TableExpandHeader />
144
+ {headers.map((header) => (
145
+ <TableHeader {...getHeaderProps({ header })}>{header.header}</TableHeader>
154
146
  ))}
155
147
  </TableRow>
156
- ))}
157
- </TableBody>
158
- </Table>
159
- {rows.length === 0 && (
160
- <p
161
- style={{ height: desktopView ? '2rem' : '3rem' }}
162
- className={`${styles.emptyRow} ${styles.bodyLong01}`}>
163
- {t('noVisitsFound', 'No visits found')}
164
- </p>
165
- )}
166
- <Pagination
167
- forwardText=""
168
- backwardText=""
169
- page={currentPage}
170
- pageSize={currentPageSize}
171
- pageSizes={pageSizes}
172
- totalItems={searchResults.length}
173
- className={styles.pagination}
174
- onChange={({ pageSize, page }) => {
175
- if (pageSize !== currentPageSize) {
176
- setPageSize(pageSize);
177
- }
178
- if (page !== currentPage) {
179
- goTo(page);
180
- }
181
- }}
182
- />
183
- </TableContainer>
184
- )}
185
- </DataTable>
186
- </div>
187
- ) : (
188
- <DataTableSkeleton />
189
- );
148
+ </TableHead>
149
+ <TableBody>
150
+ {rows.map((row, index) => (
151
+ <React.Fragment key={index}>
152
+ <TableExpandRow {...getRowProps({ row })}>
153
+ {row.cells.map((cell) => (
154
+ <TableCell key={cell.id}>
155
+ {cell.info.header === 'name' ? (
156
+ <ConfigurableLink
157
+ to={`\${openmrsSpaBase}/patient/${paginatedActiveVisits?.[index]?.patientUuid}/chart/`}>
158
+ {cell.value}
159
+ </ConfigurableLink>
160
+ ) : (
161
+ cell.value
162
+ )}
163
+ </TableCell>
164
+ ))}
165
+ </TableExpandRow>
166
+ {row.isExpanded && (
167
+ <TableRow className={styles.expandedActiveVisitRow}>
168
+ <th colSpan={headers.length + 2}>
169
+ <ExtensionSlot
170
+ className={styles.visitSummaryContainer}
171
+ extensionSlotName="visit-summary-slot"
172
+ state={{
173
+ visitUuid: paginatedActiveVisits[index]?.visitUuid,
174
+ patientUuid: paginatedActiveVisits[index]?.patientUuid,
175
+ }}
176
+ />
177
+ </th>
178
+ </TableRow>
179
+ )}
180
+ </React.Fragment>
181
+ ))}
182
+ </TableBody>
183
+ </Table>
184
+ {rows.length === 0 && (
185
+ <p
186
+ style={{ height: desktopView ? '2rem' : '3rem', marginLeft: desktopView ? '2rem' : '3rem' }}
187
+ className={`${styles.emptyRow} ${styles.bodyLong01}`}>
188
+ {t('noVisitsFound', 'No visits found')}
189
+ </p>
190
+ )}
191
+ <Pagination
192
+ forwardText="Next page"
193
+ backwardText="Previous page"
194
+ page={currentPage}
195
+ pageSize={currentPageSize}
196
+ pageSizes={pageSizes}
197
+ totalItems={searchResults.length}
198
+ className={styles.pagination}
199
+ onChange={({ pageSize, page }) => {
200
+ if (pageSize !== currentPageSize) {
201
+ setPageSize(pageSize);
202
+ }
203
+ if (page !== currentPage) {
204
+ goTo(page);
205
+ }
206
+ }}
207
+ />
208
+ </TableContainer>
209
+ )}
210
+ </DataTable>
211
+ </div>
212
+ );
213
+ }
190
214
  };
191
215
 
216
+ function formatDatetime(startDatetime) {
217
+ const dateToday = dayjs();
218
+ const today =
219
+ dayjs(startDatetime).get('date') === dateToday.get('date') &&
220
+ dayjs(startDatetime).get('month') === dateToday.get('month') &&
221
+ dayjs(startDatetime).get('year') === dateToday.get('year');
222
+ if (today) {
223
+ return `Today - ${dayjs(startDatetime).format('HH:mm')}`;
224
+ } else {
225
+ return dayjs(startDatetime).format("DD MMM 'YY - HH:mm");
226
+ }
227
+ }
228
+
192
229
  export default ActiveVisitsTable;
@@ -1,27 +1,47 @@
1
- import { FetchResponse, openmrsFetch, openmrsObservableFetch, OpenmrsResource } from '@openmrs/esm-framework';
2
- import { take, map } from 'rxjs/operators';
1
+ import useSWR from 'swr';
2
+ import { openmrsFetch, Visit } from '@openmrs/esm-framework';
3
3
 
4
- export interface ActiveVisitRow {
4
+ export interface ActiveVisit {
5
+ age: string;
5
6
  id: string;
6
- visitStartTime: string;
7
- IDNumber: string;
8
- name: string;
7
+ idNumber: string;
9
8
  gender: string;
10
- age: string;
11
- visitType: string;
9
+ name: string;
12
10
  patientUuid: string;
11
+ visitStartTime: string;
12
+ visitType: string;
13
+ visitUuid: string;
13
14
  }
14
15
 
15
- export function fetchActiveVisits() {
16
- const v =
16
+ export function useActiveVisits() {
17
+ const customRepresentation =
17
18
  'custom:(uuid,patient:(uuid,identifiers:(identifier,uuid),person:(age,display,gender,uuid)),' +
18
19
  'visitType:(uuid,name,display),location:(uuid,name,display),startDatetime,' +
19
20
  'stopDatetime)';
20
- return openmrsObservableFetch(`/ws/rest/v1/visit?includeInactive=false&v=${v}`, {
21
- headers: {
22
- contentType: 'application/json',
23
- },
24
- })
25
- .pipe(take(1))
26
- .pipe(map((response: FetchResponse<{ results: Array<any> }>) => response.data));
21
+
22
+ const { data, error, isValidating } = useSWR<{ data: { results: Array<Visit> } }, Error>(
23
+ `/ws/rest/v1/visit?includeInactive=false&v=${customRepresentation}`,
24
+ openmrsFetch,
25
+ );
26
+
27
+ const mapVisitProperties = (visit: Visit): ActiveVisit => ({
28
+ age: visit?.patient?.person?.age,
29
+ id: visit.uuid,
30
+ idNumber: visit?.patient?.identifiers[0]?.identifier,
31
+ gender: visit?.patient?.person?.gender,
32
+ name: visit?.patient?.person?.display,
33
+ patientUuid: visit?.patient?.uuid,
34
+ visitStartTime: visit.startDate,
35
+ visitType: visit?.visitType?.display,
36
+ visitUuid: visit.uuid,
37
+ });
38
+
39
+ const formattedActiveVisits = data?.data?.results.length ? data.data.results.map(mapVisitProperties) : [];
40
+
41
+ return {
42
+ data: formattedActiveVisits,
43
+ isLoading: !data && !error,
44
+ isError: error,
45
+ isValidating,
46
+ };
27
47
  }
@@ -16,6 +16,13 @@
16
16
  background-color: $ui-background;
17
17
  }
18
18
 
19
+ .backgroundDataFetchingIndicator {
20
+ align-items: center;
21
+ display: flex;
22
+ flex: 1 1 0%;
23
+ justify-content: center;
24
+ }
25
+
19
26
  .productiveHeading02::after {
20
27
  content: "";
21
28
  display: block;
@@ -41,3 +48,30 @@
41
48
  display: flex;
42
49
  align-items: center;
43
50
  }
51
+
52
+ .activeVisitsTable tr[data-parent-row]:nth-child(odd) td {
53
+ background-color: #ffffff;
54
+ }
55
+
56
+ .activeVisitsTable tbody tr[data-parent-row]:nth-child(even) td {
57
+ background-color: #f4f4f4;
58
+ }
59
+
60
+ .visitSummaryContainer {
61
+ width: 100%;
62
+ max-width: 768px;
63
+ margin: 1rem auto;
64
+ }
65
+
66
+ .expandedActiveVisitRow > td > div {
67
+ max-height: max-content !important;
68
+ }
69
+
70
+ .expandedActiveVisitRow td {
71
+ padding: 0 2rem;
72
+ }
73
+
74
+ .expandedActiveVisitRow th[colspan] td[colspan] > div:first-child {
75
+ padding: 0 1rem;
76
+ }
77
+
package/src/index.ts CHANGED
@@ -28,6 +28,11 @@ function setupOpenMRS() {
28
28
  slot: 'homepage-widgets-slot',
29
29
  load: getAsyncLifecycle(() => import('./active-visits-widget/active-visits.component'), options),
30
30
  },
31
+ {
32
+ id: 'visit-summary-widget',
33
+ slot: 'visit-summary-slot',
34
+ load: getAsyncLifecycle(() => import('./visits-summary/visit-detail.component'), options),
35
+ },
31
36
  ],
32
37
  };
33
38
  }
package/src/root.scss CHANGED
@@ -12,3 +12,19 @@
12
12
  .bodyLong01 {
13
13
  @include carbon--type-style("body-long-01");
14
14
  }
15
+
16
+ .caption01 {
17
+ @include carbon--type-style("caption-01");
18
+ }
19
+
20
+ .bodyShort02 {
21
+ @include carbon--type-style("body-short-02");
22
+ }
23
+
24
+ .text02 {
25
+ color: #525252;
26
+ }
27
+
28
+ .text01 {
29
+ color: #161616;
30
+ }