@openmrs/esm-dispensing-app 1.9.1 → 1.9.2-pre.1002

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 (179) hide show
  1. package/.eslintrc +8 -0
  2. package/.turbo/cache/f95995cd573a4ae0-meta.json +1 -0
  3. package/.turbo/cache/f95995cd573a4ae0.tar.zst +0 -0
  4. package/.turbo/turbo-build.log +38 -0
  5. package/README.md +14 -4
  6. package/dist/1119.js +1 -1
  7. package/dist/1197.js +1 -1
  8. package/dist/1856.js +1 -0
  9. package/dist/1856.js.map +1 -0
  10. package/dist/2146.js +1 -1
  11. package/dist/2690.js +1 -1
  12. package/dist/2898.js +1 -1
  13. package/dist/2898.js.map +1 -1
  14. package/dist/3099.js +1 -1
  15. package/dist/3584.js +1 -1
  16. package/dist/4055.js +1 -1
  17. package/dist/4099.js +1 -1
  18. package/dist/4099.js.map +1 -1
  19. package/dist/4132.js +1 -1
  20. package/dist/4300.js +1 -1
  21. package/dist/4335.js +1 -1
  22. package/dist/445.js +2 -0
  23. package/dist/445.js.map +1 -0
  24. package/dist/4618.js +1 -1
  25. package/dist/4652.js +1 -1
  26. package/dist/4944.js +1 -1
  27. package/dist/4999.js +2 -0
  28. package/dist/4999.js.map +1 -0
  29. package/dist/5173.js +1 -1
  30. package/dist/5241.js +1 -1
  31. package/dist/5379.js +1 -0
  32. package/dist/5379.js.map +1 -0
  33. package/dist/5442.js +1 -1
  34. package/dist/561.js +1 -0
  35. package/dist/561.js.map +1 -0
  36. package/dist/5661.js +1 -1
  37. package/dist/5897.js +1 -1
  38. package/dist/5897.js.map +1 -1
  39. package/dist/6022.js +1 -1
  40. package/dist/609.js +1 -1
  41. package/dist/609.js.map +1 -1
  42. package/dist/6468.js +1 -1
  43. package/dist/6540.js +1 -1
  44. package/dist/6540.js.map +1 -1
  45. package/dist/6679.js +1 -1
  46. package/dist/67.js +1 -1
  47. package/dist/67.js.map +1 -1
  48. package/dist/6840.js +1 -1
  49. package/dist/6841.js +1 -1
  50. package/dist/6841.js.map +1 -1
  51. package/dist/6859.js +1 -1
  52. package/dist/7097.js +1 -1
  53. package/dist/7159.js +1 -1
  54. package/dist/723.js +1 -1
  55. package/dist/7255.js +1 -0
  56. package/dist/7255.js.map +1 -0
  57. package/dist/7617.js +1 -1
  58. package/dist/795.js +1 -1
  59. package/dist/8163.js +1 -1
  60. package/dist/8349.js +1 -1
  61. package/dist/8600.js +1 -1
  62. package/dist/8600.js.map +1 -1
  63. package/dist/8618.js +1 -1
  64. package/dist/890.js +1 -1
  65. package/dist/9214.js +1 -1
  66. package/dist/9538.js +1 -1
  67. package/dist/9569.js +1 -1
  68. package/dist/961.js +1 -1
  69. package/dist/961.js.map +1 -1
  70. package/dist/963.js +1 -1
  71. package/dist/963.js.map +1 -1
  72. package/dist/986.js +1 -1
  73. package/dist/9879.js +1 -1
  74. package/dist/9895.js +1 -1
  75. package/dist/9900.js +1 -1
  76. package/dist/9913.js +1 -1
  77. package/dist/main.js +1 -1
  78. package/dist/main.js.map +1 -1
  79. package/dist/openmrs-esm-dispensing-app.js +1 -1
  80. package/dist/openmrs-esm-dispensing-app.js.buildmanifest.json +302 -323
  81. package/dist/openmrs-esm-dispensing-app.js.map +1 -1
  82. package/dist/routes.json +1 -1
  83. package/e2e/commands/drug-order-operations.ts +2 -3
  84. package/e2e/commands/encounter-operations.ts +4 -8
  85. package/e2e/commands/index.ts +3 -3
  86. package/e2e/commands/patient-operations.ts +1 -1
  87. package/e2e/commands/provider-operations.ts +2 -5
  88. package/e2e/commands/visit-operations.ts +1 -1
  89. package/e2e/fixtures/api.ts +1 -1
  90. package/e2e/specs/close-prescription.spec.ts +5 -0
  91. package/e2e/specs/dispense-medication.spec.ts +4 -0
  92. package/e2e/specs/pause-prescription.spec.ts +5 -0
  93. package/e2e/support/github/Dockerfile +1 -1
  94. package/e2e/support/github/run-e2e-docker-env.sh +23 -29
  95. package/package.json +12 -11
  96. package/src/components/action-buttons.component.tsx +8 -3
  97. package/src/components/action-buttons.test.tsx +3 -0
  98. package/src/components/prescription-actions/close-action-button.component.tsx +3 -1
  99. package/src/components/prescription-actions/dispense-action-button.component.tsx +3 -1
  100. package/src/components/prescription-actions/pause-action-button.component.tsx +3 -1
  101. package/src/dashboard/dispensing-dashboard.component.tsx +3 -0
  102. package/src/fill-prescription/fill-prescription-button.comonent.tsx +43 -0
  103. package/src/fill-prescription/fill-prescription-button.scss +8 -0
  104. package/src/forms/close-dispense-form.workspace.tsx +2 -1
  105. package/src/forms/dispense-form.workspace.tsx +16 -16
  106. package/src/forms/forms.scss +15 -0
  107. package/src/forms/medication-dispense-review.component.tsx +34 -87
  108. package/src/forms/medication-dispense-review.test.tsx +1 -1
  109. package/src/forms/pause-dispense-form.workspace.tsx +2 -1
  110. package/src/history/history-and-comments.component.tsx +2 -0
  111. package/src/medication-dispense/medication-dispense.resource.tsx +11 -9
  112. package/src/medication-request/medication-request.resource.test.tsx +7 -3
  113. package/src/medication-request/medication-request.resource.tsx +47 -44
  114. package/src/prescriptions/patient-search-tab-panel.component.tsx +58 -0
  115. package/src/prescriptions/patient-search-tab-panel.scss +26 -0
  116. package/src/prescriptions/prescription-details.component.tsx +3 -1
  117. package/src/prescriptions/prescription-tab-lists.component.tsx +18 -84
  118. package/src/prescriptions/prescription-tab-panel.component.tsx +45 -148
  119. package/src/prescriptions/prescriptions-table.component.tsx +164 -0
  120. package/src/routes.json +2 -2
  121. package/src/types.ts +10 -1
  122. package/src/utils.ts +31 -4
  123. package/tools/setup-tests.ts +7 -0
  124. package/translations/am.json +3 -0
  125. package/translations/ar.json +3 -0
  126. package/translations/ar_SY.json +3 -0
  127. package/translations/bn.json +3 -0
  128. package/translations/de.json +3 -0
  129. package/translations/en.json +3 -0
  130. package/translations/en_US.json +3 -0
  131. package/translations/es.json +3 -0
  132. package/translations/es_MX.json +3 -0
  133. package/translations/fr.json +3 -0
  134. package/translations/he.json +3 -0
  135. package/translations/hi.json +3 -0
  136. package/translations/hi_IN.json +3 -0
  137. package/translations/id.json +3 -0
  138. package/translations/it.json +28 -25
  139. package/translations/ka.json +3 -0
  140. package/translations/km.json +3 -0
  141. package/translations/ku.json +3 -0
  142. package/translations/ky.json +3 -0
  143. package/translations/lg.json +3 -0
  144. package/translations/ne.json +3 -0
  145. package/translations/pl.json +3 -0
  146. package/translations/pt.json +3 -0
  147. package/translations/pt_BR.json +3 -0
  148. package/translations/qu.json +3 -0
  149. package/translations/ro_RO.json +3 -0
  150. package/translations/ru_RU.json +3 -0
  151. package/translations/si.json +3 -0
  152. package/translations/sw.json +3 -0
  153. package/translations/sw_KE.json +3 -0
  154. package/translations/tr.json +3 -0
  155. package/translations/tr_TR.json +3 -0
  156. package/translations/uk.json +3 -0
  157. package/translations/uz.json +3 -0
  158. package/translations/uz@Latn.json +3 -0
  159. package/translations/uz_UZ.json +3 -0
  160. package/translations/vi.json +3 -0
  161. package/translations/zh.json +3 -0
  162. package/translations/zh_CN.json +28 -25
  163. package/dist/1033.js +0 -1
  164. package/dist/1033.js.map +0 -1
  165. package/dist/3414.js +0 -1
  166. package/dist/3414.js.map +0 -1
  167. package/dist/6411.js +0 -1
  168. package/dist/6411.js.map +0 -1
  169. package/dist/6741.js +0 -2
  170. package/dist/6741.js.map +0 -1
  171. package/dist/7268.js +0 -2
  172. package/dist/7268.js.map +0 -1
  173. package/dist/7556.js +0 -1
  174. package/dist/7556.js.map +0 -1
  175. package/dist/7935.js +0 -1
  176. package/dist/7935.js.map +0 -1
  177. package/e2e/commands/medication-request-operation.ts +0 -97
  178. /package/dist/{7268.js.LICENSE.txt → 445.js.LICENSE.txt} +0 -0
  179. /package/dist/{6741.js.LICENSE.txt → 4999.js.LICENSE.txt} +0 -0
@@ -0,0 +1,26 @@
1
+ @use '@carbon/layout';
2
+ @use '~@openmrs/esm-styleguide/src/vars' as *;
3
+
4
+ .searchTabPanel {
5
+ display: flex;
6
+ flex-direction: column;
7
+ margin: layout.$spacing-05 layout.$spacing-04;
8
+ gap: layout.$spacing-05;
9
+ }
10
+
11
+ .searchForPatientPlaceholder {
12
+ display: flex;
13
+ justify-content: center;
14
+ align-items: center;
15
+ text-align: center;
16
+ min-height: 400px;
17
+ flex: 1;
18
+ border: solid 1px $grey-2;
19
+ }
20
+
21
+ .searchBar {
22
+ input {
23
+ background-color: $ui-02;
24
+ }
25
+ display: flex;
26
+ }
@@ -9,7 +9,7 @@ import { type PharmacyConfig } from '../config-schema';
9
9
  import { PRIVILEGE_CREATE_DISPENSE } from '../constants';
10
10
  import { usePatientAllergies, usePrescriptionDetails } from '../medication-request/medication-request.resource';
11
11
  import { type AllergyIntolerance, type MedicationRequest, MedicationRequestCombinedStatus } from '../types';
12
- import { computeMedicationRequestCombinedStatus, getConceptCodingDisplay } from '../utils';
12
+ import { computeMedicationRequestCombinedStatus, getConceptCodingDisplay, useStaleEncounterUuids } from '../utils';
13
13
  import PrescriptionsActionsFooter from './prescription-actions.component';
14
14
  import styles from './prescription-details.scss';
15
15
 
@@ -22,6 +22,7 @@ const PrescriptionDetails: React.FC<{
22
22
  const [isAllergiesLoading, setAllergiesLoadingStatus] = useState(true);
23
23
  const { allergies, totalAllergies } = usePatientAllergies(patientUuid, config.refreshInterval);
24
24
  const { medicationRequestBundles, error, isLoading } = usePrescriptionDetails(encounterUuid, config.refreshInterval);
25
+ const { staleEncounterUuids } = useStaleEncounterUuids();
25
26
 
26
27
  useEffect(() => {
27
28
  if (typeof totalAllergies == 'number') {
@@ -99,6 +100,7 @@ const PrescriptionDetails: React.FC<{
99
100
  patientUuid={patientUuid}
100
101
  encounterUuid={encounterUuid}
101
102
  medicationRequestBundle={bundle}
103
+ disabled={staleEncounterUuids.includes(encounterUuid)}
102
104
  />
103
105
  </UserHasAccess>
104
106
  <MedicationEvent
@@ -1,45 +1,14 @@
1
- import React, { useEffect, useState } from 'react';
2
- import { ComboBox, Search, Tab, Tabs, TabList, TabPanels } from '@carbon/react';
1
+ import React, { useState } from 'react';
2
+ import { Tab, Tabs, TabList, TabPanels } from '@carbon/react';
3
3
  import { useTranslation } from 'react-i18next';
4
- import { useConfig } from '@openmrs/esm-framework';
5
- import { useLocationForFiltering } from '../location/location.resource';
6
- import { type SimpleLocation } from '../types';
7
- import { type PharmacyConfig } from '../config-schema';
8
4
  import PrescriptionTabPanel from './prescription-tab-panel.component';
9
5
  import styles from './prescriptions.scss';
6
+ import PatientSearchTabPanel from './patient-search-tab-panel.component';
10
7
 
11
8
  const PrescriptionTabLists: React.FC = () => {
12
9
  const { t } = useTranslation();
13
- const config = useConfig<PharmacyConfig>();
14
- const { filterLocations, isLoading: isFilterLocationsLoading } = useLocationForFiltering(config);
15
- const [searchTermUserInput, setSearchTermUserInput] = useState(''); // we have a separate "searchTermUserInput" and "searchTerm" in order to debounce
16
- const [searchTerm, setSearchTerm] = useState('');
17
- const [location, setLocation] = useState('');
18
10
  const [selectedTab, setSelectedTab] = useState(0);
19
11
 
20
- const tabs = [
21
- {
22
- key: 'activePrescriptions',
23
- header: t('activePrescriptions', 'Active Prescriptions'),
24
- status: 'ACTIVE',
25
- },
26
- {
27
- key: 'allPrescriptions',
28
- header: t('allPrescriptions', 'All Prescriptions'),
29
- status: '',
30
- },
31
- ];
32
-
33
- // debounce: delay the search term update so that a search isn't triggered on every single keystroke
34
- useEffect(() => {
35
- const debounceFn = setTimeout(() => {
36
- setSearchTerm(searchTermUserInput);
37
- }, 500);
38
-
39
- return () => clearTimeout(debounceFn);
40
- }, [searchTermUserInput]);
41
-
42
- // we use this to only render the tab panel that is currently selected, see O3-2777
43
12
  const handleTabChange = (event) => {
44
13
  setSelectedTab(event.selectedIndex);
45
14
  };
@@ -49,58 +18,23 @@ const PrescriptionTabLists: React.FC = () => {
49
18
  <section className={styles.prescriptionTabsContainer}>
50
19
  <Tabs onChange={handleTabChange}>
51
20
  <TabList aria-label={t('tabList', 'Tab List')} contained className={styles.tabsContainer}>
52
- {tabs.map((tab, index) => {
53
- return (
54
- <Tab title={t(tab.key)} key={index} id={'tab-' + index} className={styles.tab}>
55
- {t(tab.header)}
56
- </Tab>
57
- );
58
- })}
21
+ <Tab title={t('search', 'Search')} id={'tab-search'} className={styles.tab}>
22
+ {t('search', 'Search')}
23
+ </Tab>
24
+ <Tab
25
+ title={t('activePrescriptions', 'Active Prescriptions')}
26
+ id={'tab-active-prescription'}
27
+ className={styles.tab}>
28
+ {t('activePrescriptions', 'Active Prescriptions')}
29
+ </Tab>
30
+ <Tab title={t('allPrescriptions', 'All Prescriptions')} id={'tab-all-prescription'} className={styles.tab}>
31
+ {t('allPrescriptions', 'All Prescriptions')}
32
+ </Tab>
59
33
  </TabList>
60
- <div className={styles.searchContainer}>
61
- {/* <Button
62
- kind="primary"
63
- renderIcon={(props) => <Add size={24} />}
64
- className={styles.addPrescriptionBtn}
65
- size="sm"
66
- >
67
- {t("fillPrescription", "Fill prescription")}
68
- </Button>*/}
69
- <Search
70
- closeButtonLabelText={t('clearSearchInput', 'Clear search input')}
71
- defaultValue={searchTermUserInput}
72
- placeholder={t('searchByPatientIdOrName', 'Search by patient ID or name')}
73
- labelText={t('searchByPatientIdOrName', 'Search by patient ID or name')}
74
- onChange={(e) => {
75
- e.preventDefault();
76
- setSearchTermUserInput(e.target.value);
77
- }}
78
- size="md"
79
- className={styles.patientSearch}
80
- />
81
- {config.locationBehavior?.locationFilter?.enabled &&
82
- !isFilterLocationsLoading &&
83
- filterLocations?.length > 1 && (
84
- <ComboBox
85
- id="locationFilter"
86
- placeholder={t('filterByLocation', 'Filter by location')}
87
- items={isFilterLocationsLoading ? [] : filterLocations}
88
- itemToString={(item: SimpleLocation) => item?.name}
89
- onChange={({ selectedItem }) => {
90
- setLocation(selectedItem?.id);
91
- }}
92
- className={styles.locationFilter}
93
- />
94
- )}
95
- </div>
96
34
  <TabPanels>
97
- {tabs.map((tab, index) => {
98
- return index === selectedTab ? (
99
- <PrescriptionTabPanel location={location} searchTerm={searchTerm} status={tab.status} />
100
- ) : (
101
- <></>
102
- );
103
- })}
35
+ <PatientSearchTabPanel />
36
+ <PrescriptionTabPanel isTabActive={selectedTab === 1} status={'ACTIVE'} />
37
+ <PrescriptionTabPanel isTabActive={selectedTab === 2} status={''} />
104
38
  </TabPanels>
105
39
  </Tabs>
106
40
  </section>
@@ -1,165 +1,62 @@
1
- import {
2
- DataTable,
3
- DataTableSkeleton,
4
- Layer,
5
- Pagination,
6
- Table,
7
- TableBody,
8
- TableCell,
9
- TableContainer,
10
- TableExpandedRow,
11
- TableExpandHeader,
12
- TableExpandRow,
13
- TableHead,
14
- TableHeader,
15
- TableRow,
16
- TabPanel,
17
- Tile,
18
- } from '@carbon/react';
19
- import { formatDatetime, parseDate, useConfig } from '@openmrs/esm-framework';
20
- import React, { useEffect, useState } from 'react';
1
+ import React, { useState } from 'react';
21
2
  import { useTranslation } from 'react-i18next';
3
+ import { ComboBox, Search, TabPanel } from '@carbon/react';
4
+ import { useConfig, useDebounce } from '@openmrs/esm-framework';
22
5
  import { type PharmacyConfig } from '../config-schema';
23
- import { usePrescriptionsTable } from '../medication-request/medication-request.resource';
24
- import PatientInfoCell from '../patient/patient-info-cell.component';
25
- import PrescriptionExpanded from './prescription-expanded.component';
6
+ import PrescriptionsTable from './prescriptions-table.component';
26
7
  import styles from './prescriptions.scss';
8
+ import { useLocationForFiltering } from '../location/location.resource';
9
+ import { type SimpleLocation } from '../types';
27
10
 
28
11
  interface PrescriptionTabPanelProps {
29
- searchTerm: string;
30
- location: string;
31
12
  status: string;
13
+ isTabActive: boolean;
32
14
  }
33
15
 
34
- const PrescriptionTabPanel: React.FC<PrescriptionTabPanelProps> = ({ searchTerm, location, status }) => {
16
+ const PrescriptionTabPanel: React.FC<PrescriptionTabPanelProps> = ({ status, isTabActive }) => {
35
17
  const { t } = useTranslation();
36
18
  const config = useConfig<PharmacyConfig>();
37
- const [page, setPage] = useState(1);
38
- const [pageSize, setPageSize] = useState(10);
39
- const [nextOffSet, setNextOffSet] = useState(0);
40
- const { prescriptionsTableRows, error, isLoading, totalOrders } = usePrescriptionsTable(
41
- pageSize,
42
- nextOffSet,
43
- searchTerm,
44
- location,
45
- status,
46
- config.medicationRequestExpirationPeriodInDays,
47
- config.refreshInterval,
48
- );
49
-
50
- // dynamic status keys we need to maintain
51
- // t('active', 'Active')
52
- // t('paused', 'Paused')
53
- // t('closed', 'Closed')
54
- // t('completed', 'Completed')
55
- // t('expired', 'Expired')
56
- // t('cancelled', 'Cancelled')
57
-
58
- let columns = [
59
- { header: t('created', 'Created'), key: 'created' },
60
- { header: t('patientName', 'Patient name'), key: 'patient' },
61
- { header: t('prescriber', 'Prescriber'), key: 'prescriber' },
62
- { header: t('drugs', 'Drugs'), key: 'drugs' },
63
- { header: t('lastDispenser', 'Last dispenser'), key: 'lastDispenser' },
64
- { header: t('status', 'Status'), key: 'status' },
65
- ];
66
-
67
- // add the locations column, if enabled
68
- if (config.locationBehavior?.locationColumn?.enabled) {
69
- columns = [...columns.slice(0, 3), { header: t('location', 'Location'), key: 'location' }, ...columns.slice(3)];
70
- }
71
-
72
- // reset back to page 1 whenever search term changes
73
- useEffect(() => {
74
- setPage(1);
75
- setNextOffSet(0);
76
- }, [searchTerm]);
19
+ const { filterLocations, isLoading: isFilterLocationsLoading } = useLocationForFiltering(config);
20
+ const [searchTerm, setSearchTerm] = useState('');
21
+ const debouncedSearchTerm = useDebounce(searchTerm, 500);
22
+ const [location, setLocation] = useState('');
77
23
 
78
24
  return (
79
25
  <TabPanel>
80
- <div className={styles.patientListTableContainer}>
81
- {isLoading && <DataTableSkeleton role="progressbar" />}
82
- {error && <p>Error</p>}
83
- {prescriptionsTableRows && (
84
- <>
85
- <DataTable rows={prescriptionsTableRows} headers={columns} isSortable>
86
- {({ rows, headers, getExpandHeaderProps, getHeaderProps, getRowProps, getTableProps }) => (
87
- <TableContainer>
88
- <Table {...getTableProps()} useZebraStyles>
89
- <TableHead>
90
- <TableRow>
91
- <TableExpandHeader {...getExpandHeaderProps()} />
92
- {headers.map((header) => (
93
- <TableHeader {...getHeaderProps({ header })}>{header.header}</TableHeader>
94
- ))}
95
- </TableRow>
96
- </TableHead>
97
- <TableBody>
98
- {rows.map((row) => (
99
- <React.Fragment key={row.id}>
100
- <TableExpandRow {...getRowProps({ row })}>
101
- {row.cells.map((cell) => (
102
- <TableCell key={cell.id}>
103
- {cell.id.endsWith('created') ? (
104
- formatDatetime(parseDate(cell.value))
105
- ) : cell.id.endsWith('patient') ? (
106
- <PatientInfoCell patient={cell.value} />
107
- ) : cell.id.endsWith('status') ? (
108
- t(cell.value)
109
- ) : (
110
- cell.value
111
- )}
112
- </TableCell>
113
- ))}
114
- </TableExpandRow>
115
- {row.isExpanded ? (
116
- <TableExpandedRow colSpan={headers.length + 1}>
117
- <PrescriptionExpanded
118
- encounterUuid={row.id}
119
- patientUuid={row.cells.find((cell) => cell.id.endsWith('patient')).value.uuid}
120
- status={row.cells.find((cell) => cell.id.endsWith('status')).value}
121
- />
122
- </TableExpandedRow>
123
- ) : (
124
- <TableExpandedRow className={styles.hiddenRow} colSpan={headers.length + 2} />
125
- )}
126
- </React.Fragment>
127
- ))}
128
- </TableBody>
129
- </Table>
130
- </TableContainer>
131
- )}
132
- </DataTable>
133
- {prescriptionsTableRows?.length === 0 && (
134
- <div className={styles.filterEmptyState}>
135
- <Layer>
136
- <Tile className={styles.filterEmptyStateTile}>
137
- <p className={styles.filterEmptyStateContent}>
138
- {t('noPrescriptionsToDisplay', 'No prescriptions to display')}
139
- </p>
140
- <p className={styles.filterEmptyStateHelper}>{t('checkFilters', 'Check the filters above')}</p>
141
- </Tile>
142
- </Layer>
143
- </div>
144
- )}
145
- {prescriptionsTableRows?.length > 0 && (
146
- <div style={{ width: '100%' }}>
147
- <Pagination
148
- page={page}
149
- pageSize={pageSize}
150
- pageSizes={[10, 20, 30, 40, 50, 100]}
151
- totalItems={totalOrders}
152
- onChange={({ page, pageSize }) => {
153
- setPage(page);
154
- setNextOffSet((page - 1) * pageSize);
155
- setPageSize(pageSize);
156
- }}
157
- />
158
- </div>
159
- )}
160
- </>
161
- )}
26
+ <div className={styles.searchContainer}>
27
+ <Search
28
+ closeButtonLabelText={t('clearSearchInput', 'Clear search input')}
29
+ defaultValue={searchTerm}
30
+ placeholder={t('searchByPatientIdOrName', 'Search by patient ID or name')}
31
+ labelText={t('searchByPatientIdOrName', 'Search by patient ID or name')}
32
+ onChange={(e) => {
33
+ e.preventDefault();
34
+ setSearchTerm(e.target.value);
35
+ }}
36
+ size="md"
37
+ className={styles.patientSearch}
38
+ />
39
+ {config.locationBehavior?.locationFilter?.enabled &&
40
+ !isFilterLocationsLoading &&
41
+ filterLocations?.length > 1 && (
42
+ <ComboBox
43
+ id="locationFilter"
44
+ placeholder={t('filterByLocation', 'Filter by location')}
45
+ items={isFilterLocationsLoading ? [] : filterLocations}
46
+ itemToString={(item: SimpleLocation) => item?.name}
47
+ onChange={({ selectedItem }) => {
48
+ setLocation(selectedItem?.id);
49
+ }}
50
+ className={styles.locationFilter}
51
+ />
52
+ )}
162
53
  </div>
54
+ <PrescriptionsTable
55
+ loadData={isTabActive}
56
+ status={status}
57
+ debouncedSearchTerm={debouncedSearchTerm}
58
+ location={location}
59
+ />
163
60
  </TabPanel>
164
61
  );
165
62
  };
@@ -0,0 +1,164 @@
1
+ import {
2
+ DataTable,
3
+ DataTableSkeleton,
4
+ Layer,
5
+ Pagination,
6
+ Table,
7
+ TableBody,
8
+ TableCell,
9
+ TableContainer,
10
+ TableExpandedRow,
11
+ TableExpandHeader,
12
+ TableExpandRow,
13
+ TableHead,
14
+ TableHeader,
15
+ TableRow,
16
+ Tile,
17
+ } from '@carbon/react';
18
+ import { formatDatetime, parseDate, useConfig } from '@openmrs/esm-framework';
19
+ import React, { useEffect, useState } from 'react';
20
+ import { useTranslation } from 'react-i18next';
21
+ import PatientInfoCell from '../patient/patient-info-cell.component';
22
+ import PrescriptionExpanded from './prescription-expanded.component';
23
+ import styles from './prescriptions.scss';
24
+ import { usePrescriptionsTable } from '../medication-request/medication-request.resource';
25
+ import { type PharmacyConfig } from '../config-schema';
26
+
27
+ interface PrescriptionsTableProps {
28
+ loadData: boolean;
29
+ debouncedSearchTerm: string;
30
+ location: string;
31
+ status: string;
32
+ }
33
+
34
+ const PrescriptionsTable: React.FC<PrescriptionsTableProps> = ({ loadData, debouncedSearchTerm, location, status }) => {
35
+ const { t } = useTranslation();
36
+ const config = useConfig<PharmacyConfig>();
37
+ const [page, setPage] = useState(1);
38
+ const [pageSize, setPageSize] = useState(10);
39
+ const nextOffSet = (page - 1) * pageSize;
40
+ const { prescriptionsTableRows, error, isLoading, totalOrders } = usePrescriptionsTable(
41
+ loadData,
42
+ pageSize,
43
+ nextOffSet,
44
+ debouncedSearchTerm,
45
+ location,
46
+ status,
47
+ config.medicationRequestExpirationPeriodInDays,
48
+ config.refreshInterval,
49
+ );
50
+
51
+ // reset back to page 1 whenever search term changes
52
+ useEffect(() => {
53
+ setPage(1);
54
+ }, [debouncedSearchTerm]);
55
+
56
+ // dynamic status keys we need to maintain
57
+ // t('active', 'Active')
58
+ // t('paused', 'Paused')
59
+ // t('closed', 'Closed')
60
+ // t('completed', 'Completed')
61
+ // t('expired', 'Expired')
62
+ // t('cancelled', 'Cancelled')
63
+
64
+ let columns = [
65
+ { header: t('created', 'Created'), key: 'created' },
66
+ { header: t('patientName', 'Patient name'), key: 'patient' },
67
+ { header: t('prescriber', 'Prescriber'), key: 'prescriber' },
68
+ { header: t('drugs', 'Drugs'), key: 'drugs' },
69
+ { header: t('lastDispenser', 'Last dispenser'), key: 'lastDispenser' },
70
+ { header: t('status', 'Status'), key: 'status' },
71
+ ];
72
+
73
+ // add the locations column, if enabled
74
+ if (config.locationBehavior?.locationColumn?.enabled) {
75
+ columns = [...columns.slice(0, 3), { header: t('location', 'Location'), key: 'location' }, ...columns.slice(3)];
76
+ }
77
+
78
+ return (
79
+ <div className={styles.patientListTableContainer}>
80
+ {isLoading && <DataTableSkeleton role="progressbar" />}
81
+ {error && <p>Error</p>}
82
+ {prescriptionsTableRows && (
83
+ <>
84
+ <DataTable rows={prescriptionsTableRows} headers={columns} isSortable>
85
+ {({ rows, headers, getExpandHeaderProps, getHeaderProps, getRowProps, getTableProps }) => (
86
+ <TableContainer>
87
+ <Table {...getTableProps()} useZebraStyles>
88
+ <TableHead>
89
+ <TableRow>
90
+ <TableExpandHeader {...getExpandHeaderProps()} />
91
+ {headers.map((header) => (
92
+ <TableHeader {...getHeaderProps({ header })}>{header.header}</TableHeader>
93
+ ))}
94
+ </TableRow>
95
+ </TableHead>
96
+ <TableBody>
97
+ {rows.map((row) => (
98
+ <React.Fragment key={row.id}>
99
+ <TableExpandRow {...getRowProps({ row })}>
100
+ {row.cells.map((cell) => (
101
+ <TableCell key={cell.id}>
102
+ {cell.id.endsWith('created') ? (
103
+ formatDatetime(parseDate(cell.value))
104
+ ) : cell.id.endsWith('patient') ? (
105
+ <PatientInfoCell patient={cell.value} />
106
+ ) : cell.id.endsWith('status') ? (
107
+ t(cell.value)
108
+ ) : (
109
+ cell.value
110
+ )}
111
+ </TableCell>
112
+ ))}
113
+ </TableExpandRow>
114
+ {row.isExpanded ? (
115
+ <TableExpandedRow colSpan={headers.length + 1}>
116
+ <PrescriptionExpanded
117
+ encounterUuid={row.id}
118
+ patientUuid={row.cells.find((cell) => cell.id.endsWith('patient')).value.uuid}
119
+ status={row.cells.find((cell) => cell.id.endsWith('status')).value}
120
+ />
121
+ </TableExpandedRow>
122
+ ) : (
123
+ <TableExpandedRow className={styles.hiddenRow} colSpan={headers.length + 2} />
124
+ )}
125
+ </React.Fragment>
126
+ ))}
127
+ </TableBody>
128
+ </Table>
129
+ </TableContainer>
130
+ )}
131
+ </DataTable>
132
+ {prescriptionsTableRows?.length === 0 && (
133
+ <div className={styles.filterEmptyState}>
134
+ <Layer>
135
+ <Tile className={styles.filterEmptyStateTile}>
136
+ <p className={styles.filterEmptyStateContent}>
137
+ {t('noPrescriptionsToDisplay', 'No prescriptions to display')}
138
+ </p>
139
+ <p className={styles.filterEmptyStateHelper}>{t('checkFilters', 'Check the filters above')}</p>
140
+ </Tile>
141
+ </Layer>
142
+ </div>
143
+ )}
144
+ {prescriptionsTableRows?.length > 0 && (
145
+ <div style={{ width: '100%' }}>
146
+ <Pagination
147
+ page={page}
148
+ pageSize={pageSize}
149
+ pageSizes={[10, 20, 30, 40, 50, 100]}
150
+ totalItems={totalOrders}
151
+ onChange={({ page, pageSize }) => {
152
+ setPage(page);
153
+ setPageSize(pageSize);
154
+ }}
155
+ />
156
+ </div>
157
+ )}
158
+ </>
159
+ )}
160
+ </div>
161
+ );
162
+ };
163
+
164
+ export default PrescriptionsTable;
package/src/routes.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "$schema": "https://json.openmrs.org/routes.schema.json",
3
3
  "backendDependencies": {
4
4
  "fhir2": ">=1.2",
5
- "webservices.rest": "^2.2.0"
5
+ "webservices.rest": ">=2.2.0"
6
6
  },
7
7
  "pages": [
8
8
  {
@@ -98,4 +98,4 @@
98
98
 
99
99
  }
100
100
  ]
101
- }
101
+ }
package/src/types.ts CHANGED
@@ -451,7 +451,7 @@ export interface PrescriptionsTableRow {
451
451
 
452
452
  export interface Quantity {
453
453
  value: number;
454
- unit: string;
454
+ unit?: string;
455
455
  code: string;
456
456
  system?: string;
457
457
  }
@@ -523,3 +523,12 @@ export type StockDispenseRequest = {
523
523
  stockItemPackagingUOM: string;
524
524
  quantity: number;
525
525
  };
526
+
527
+ export interface DispensingStore {
528
+ /**
529
+ * A list of encounterUuids with stale data due to
530
+ * updates (like pausing of dispensing medication) actions,
531
+ * and subsequent SWR revalidation has not yet completed
532
+ */
533
+ staleEncounterUuids: string[];
534
+ }
package/src/utils.ts CHANGED
@@ -2,6 +2,7 @@ import dayjs from 'dayjs';
2
2
  import { mutate } from 'swr';
3
3
  import {
4
4
  type Coding,
5
+ type DispensingStore,
5
6
  type DosageInstruction,
6
7
  type Medication,
7
8
  type MedicationDispense,
@@ -14,7 +15,12 @@ import {
14
15
  MedicationRequestStatus,
15
16
  type Quantity,
16
17
  } from './types';
17
- import { fhirBaseUrl, parseDate } from '@openmrs/esm-framework';
18
+ import {
19
+ createGlobalStore,
20
+ fhirBaseUrl,
21
+ parseDate,
22
+ useStore,
23
+ } from '@openmrs/esm-framework';
18
24
  import {
19
25
  OPENMRS_FHIR_EXT_DISPENSE_RECORDED,
20
26
  OPENMRS_FHIR_EXT_MEDICINE,
@@ -565,16 +571,29 @@ export function isMostRecentMedicationDispense(
565
571
 
566
572
  /**
567
573
  * Revalidated (reloads) both the prescription associated with the encounter uuid,
568
- * and the entire prescrption table
574
+ * and the entire prescription table
569
575
  * @param encounterUuid
570
576
  */
571
- export function revalidate(encounterUuid: string) {
572
- mutate(
577
+ export async function revalidate(encounterUuid: string) {
578
+ await mutate(
573
579
  (key) =>
574
580
  typeof key === 'string' &&
575
581
  (key.startsWith(`${fhirBaseUrl}/${PRESCRIPTIONS_TABLE_ENDPOINT}`) ||
576
582
  key.startsWith(`${fhirBaseUrl}/${PRESCRIPTION_DETAILS_ENDPOINT}?encounter=${encounterUuid}`)),
577
583
  );
584
+ dispensingStore.setState((state) => ({
585
+ staleEncounterUuids: state.staleEncounterUuids.filter((uuid) => uuid !== encounterUuid),
586
+ }));
587
+ }
588
+
589
+ /**
590
+ * Mark the specified encounter as stale. The encounter will be unmarked on calling
591
+ * revalidate() to reload the data associated with the encounter.
592
+ */
593
+ export function markEncounterAsStale(encounterUuid: string) {
594
+ dispensingStore.setState((state) => ({
595
+ staleEncounterUuids: [...state.staleEncounterUuids, encounterUuid],
596
+ }));
578
597
  }
579
598
 
580
599
  export function sortMedicationDispensesByWhenHandedOver(a: MedicationDispense, b: MedicationDispense): number {
@@ -590,3 +609,11 @@ export function sortMedicationDispensesByWhenHandedOver(a: MedicationDispense, b
590
609
  return a.id.localeCompare(b.id); // just to enforce a standard order if two dates are equals
591
610
  }
592
611
  }
612
+
613
+ const dispensingStore = createGlobalStore<DispensingStore>('dispensing-store', {
614
+ staleEncounterUuids: [],
615
+ });
616
+
617
+ export function useStaleEncounterUuids() {
618
+ return useStore(dispensingStore);
619
+ }
@@ -1 +1,8 @@
1
1
  import '@testing-library/jest-dom';
2
+
3
+ // Mock ResizeObserver for Carbon components
4
+ global.ResizeObserver = jest.fn().mockImplementation(() => ({
5
+ observe: jest.fn(),
6
+ unobserve: jest.fn(),
7
+ disconnect: jest.fn(),
8
+ }));