@openmrs/esm-appointments-app 10.0.2 → 10.0.3-pre.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 (391) hide show
  1. package/.turbo/turbo-build.log +6 -5
  2. package/dist/1339.js +1 -0
  3. package/dist/1339.js.map +1 -0
  4. package/dist/1465.js +1 -0
  5. package/dist/1465.js.map +1 -0
  6. package/dist/1480.js +1 -0
  7. package/dist/1480.js.map +1 -0
  8. package/dist/1646.js +1 -0
  9. package/dist/1646.js.map +1 -0
  10. package/dist/1789.js +1 -0
  11. package/dist/1789.js.map +1 -0
  12. package/dist/1869.js +1 -0
  13. package/dist/1869.js.map +1 -0
  14. package/dist/1877.js +1 -0
  15. package/dist/1877.js.map +1 -0
  16. package/dist/1884.js +1 -0
  17. package/dist/1884.js.map +1 -0
  18. package/dist/1962.js +1 -0
  19. package/dist/1962.js.map +1 -0
  20. package/dist/2317.js +1 -0
  21. package/dist/2317.js.map +1 -0
  22. package/dist/2416.js +1 -0
  23. package/dist/2416.js.map +1 -0
  24. package/dist/2495.js +1 -0
  25. package/dist/2495.js.map +1 -0
  26. package/dist/2539.js +1 -0
  27. package/dist/2539.js.map +1 -0
  28. package/dist/2620.js +1 -0
  29. package/dist/2620.js.map +1 -0
  30. package/dist/2717.js +1 -0
  31. package/dist/2717.js.map +1 -0
  32. package/dist/282.js +1 -0
  33. package/dist/282.js.map +1 -0
  34. package/dist/2881.js +1 -0
  35. package/dist/2881.js.map +1 -0
  36. package/dist/3198.js +11 -0
  37. package/dist/3198.js.map +1 -0
  38. package/dist/3220.js +1 -0
  39. package/dist/3220.js.map +1 -0
  40. package/dist/3378.js +1 -0
  41. package/dist/3378.js.map +1 -0
  42. package/dist/3720.js +1 -0
  43. package/dist/3720.js.map +1 -0
  44. package/dist/3930.js +1 -0
  45. package/dist/3930.js.map +1 -0
  46. package/dist/3963.js +1 -0
  47. package/dist/3963.js.map +1 -0
  48. package/dist/3989.js +1 -0
  49. package/dist/3989.js.map +1 -0
  50. package/dist/4106.js +1 -0
  51. package/dist/4106.js.map +1 -0
  52. package/dist/4111.js +1 -0
  53. package/dist/4111.js.map +1 -0
  54. package/dist/4307.js +1 -0
  55. package/dist/4307.js.map +1 -0
  56. package/dist/434.js +1 -0
  57. package/dist/434.js.map +1 -0
  58. package/dist/4348.js +1 -0
  59. package/dist/4348.js.map +1 -0
  60. package/dist/4383.js +1 -0
  61. package/dist/4383.js.map +1 -0
  62. package/dist/4658.js +1 -0
  63. package/dist/4658.js.map +1 -0
  64. package/dist/466.js +1 -0
  65. package/dist/466.js.map +1 -0
  66. package/dist/4928.js +1 -0
  67. package/dist/4928.js.map +1 -0
  68. package/dist/5117.js +1 -0
  69. package/dist/5117.js.map +1 -0
  70. package/dist/5132.js +1 -0
  71. package/dist/5132.js.map +1 -0
  72. package/dist/5145.js +1 -0
  73. package/dist/5145.js.map +1 -0
  74. package/dist/5503.js +1 -0
  75. package/dist/5503.js.map +1 -0
  76. package/dist/556.js +1 -0
  77. package/dist/556.js.map +1 -0
  78. package/dist/5640.js +1 -0
  79. package/dist/5640.js.map +1 -0
  80. package/dist/5644.js +1 -0
  81. package/dist/5644.js.map +1 -0
  82. package/dist/5940.js +1 -0
  83. package/dist/5940.js.map +1 -0
  84. package/dist/6047.js +1 -0
  85. package/dist/6047.js.map +1 -0
  86. package/dist/6098.js +38 -0
  87. package/dist/6098.js.map +1 -0
  88. package/dist/6236.js +1 -0
  89. package/dist/6236.js.map +1 -0
  90. package/dist/635.js +1 -0
  91. package/dist/635.js.map +1 -0
  92. package/dist/6371.js +1 -0
  93. package/dist/6371.js.map +1 -0
  94. package/dist/6377.js +1 -0
  95. package/dist/6377.js.map +1 -0
  96. package/dist/6444.js +1 -0
  97. package/dist/6444.js.map +1 -0
  98. package/dist/6508.js +1 -0
  99. package/dist/6508.js.map +1 -0
  100. package/dist/6724.js +1 -0
  101. package/dist/6724.js.map +1 -0
  102. package/dist/6789.js +1 -0
  103. package/dist/6789.js.map +1 -0
  104. package/dist/689.js +1 -0
  105. package/dist/689.js.map +1 -0
  106. package/dist/6904.js +1 -0
  107. package/dist/6904.js.map +1 -0
  108. package/dist/7045.js +1 -0
  109. package/dist/7045.js.map +1 -0
  110. package/dist/7138.js +1 -0
  111. package/dist/7138.js.map +1 -0
  112. package/dist/7159.js +1 -0
  113. package/dist/7159.js.map +1 -0
  114. package/dist/7175.js +1 -0
  115. package/dist/7175.js.map +1 -0
  116. package/dist/7182.js +1 -0
  117. package/dist/7182.js.map +1 -0
  118. package/dist/7357.js +1 -0
  119. package/dist/7357.js.map +1 -0
  120. package/dist/7609.js +1 -0
  121. package/dist/7609.js.map +1 -0
  122. package/dist/7654.js +1 -0
  123. package/dist/7654.js.map +1 -0
  124. package/dist/7742.js +1 -0
  125. package/dist/7742.js.map +1 -0
  126. package/dist/7912.js +1 -0
  127. package/dist/7912.js.map +1 -0
  128. package/dist/8063.js +1 -0
  129. package/dist/8063.js.map +1 -0
  130. package/dist/8346.js +1 -0
  131. package/dist/8346.js.map +1 -0
  132. package/dist/8358.js +1 -0
  133. package/dist/8358.js.map +1 -0
  134. package/dist/8359.js +1 -0
  135. package/dist/8359.js.map +1 -0
  136. package/dist/8695.js +1 -0
  137. package/dist/8695.js.map +1 -0
  138. package/dist/8937.js +1 -0
  139. package/dist/8937.js.map +1 -0
  140. package/dist/8981.js +1 -0
  141. package/dist/8981.js.map +1 -0
  142. package/dist/903.js +1 -0
  143. package/dist/903.js.map +1 -0
  144. package/dist/9061.js +1 -0
  145. package/dist/9061.js.map +1 -0
  146. package/dist/9072.js +1 -0
  147. package/dist/9072.js.map +1 -0
  148. package/dist/9282.js +1 -0
  149. package/dist/9282.js.map +1 -0
  150. package/dist/9399.js +1 -0
  151. package/dist/9399.js.map +1 -0
  152. package/dist/9443.js +1 -0
  153. package/dist/9443.js.map +1 -0
  154. package/dist/9581.js +1 -0
  155. package/dist/9581.js.map +1 -0
  156. package/dist/9712.js +1 -0
  157. package/dist/9712.js.map +1 -0
  158. package/dist/9771.js +1 -0
  159. package/dist/9771.js.map +1 -0
  160. package/dist/9806.js +1 -0
  161. package/dist/9806.js.map +1 -0
  162. package/dist/main.js +6 -5
  163. package/dist/main.js.map +1 -1
  164. package/dist/openmrs-esm-appointments-app.js +6 -5
  165. package/dist/openmrs-esm-appointments-app.js.buildmanifest.json +625 -620
  166. package/dist/openmrs-esm-appointments-app.js.map +1 -1
  167. package/dist/routes.json +1 -1
  168. package/package.json +3 -2
  169. package/src/appointments/appointment-tabs.component.tsx +38 -16
  170. package/src/appointments/common-components/appointments-actions.component.tsx +39 -47
  171. package/src/appointments/common-components/appointments-actions.test.tsx +52 -99
  172. package/src/appointments/common-components/appointments-table.component.tsx +109 -135
  173. package/src/appointments/common-components/appointments-table.scss +10 -6
  174. package/src/appointments/common-components/appointments-table.test.tsx +2 -9
  175. package/src/appointments/common-components/batch-change-appointment-statuses.modal.tsx +1 -1
  176. package/src/appointments/common-components/batch-change-appointment-statuses.test.tsx +8 -6
  177. package/src/appointments/common-components/checkin-button.component.tsx +60 -29
  178. package/src/appointments/common-components/end-appointment.modal.tsx +1 -1
  179. package/src/appointments/common-components/end-appointment.test.tsx +1 -1
  180. package/src/appointments/scheduled/early-appointments.component.tsx +6 -7
  181. package/src/appointments/scheduled/scheduled-appointments.component.tsx +26 -206
  182. package/src/appointments/utils.tsx +29 -16
  183. package/src/appointments.component.tsx +5 -13
  184. package/src/appointments.test.tsx +16 -4
  185. package/src/calendar/appointments-calendar-view.component.tsx +3 -12
  186. package/src/calendar/appointments-calendar-view.test.tsx +6 -1
  187. package/src/calendar/header/calendar-header.component.tsx +2 -2
  188. package/src/calendar/monthly/monthly-calendar-view.component.tsx +2 -2
  189. package/src/calendar/monthly/monthly-header.component.tsx +9 -8
  190. package/src/calendar/monthly/monthly-workload-view.component.tsx +2 -2
  191. package/src/config-schema.ts +17 -9
  192. package/src/constants.ts +12 -1
  193. package/src/form/appointments-form.resource.ts +1 -120
  194. package/src/form/appointments-form.workspace.tsx +15 -18
  195. package/src/header/appointments-header.component.tsx +27 -9
  196. package/src/helpers/functions.ts +1 -50
  197. package/src/home/home-appointments.component.tsx +17 -4
  198. package/src/hooks/useActiveVisits.ts +14 -0
  199. package/src/hooks/useAppointmentList.ts +12 -29
  200. package/src/hooks/useAppointmentService.ts +6 -1
  201. package/src/hooks/useAppointmentsAppContext.ts +19 -0
  202. package/src/hooks/useAppointmentsCalendar.ts +2 -1
  203. package/src/hooks/useMutateAppointments.ts +24 -0
  204. package/src/hooks/usePatientAppointmentHistory.ts +3 -2
  205. package/src/hooks/useSelectedDate.ts +24 -0
  206. package/src/hooks/useUnscheduledAppointments.ts +5 -1
  207. package/src/index.ts +0 -21
  208. package/src/metrics/metrics-cards/highest-volume-service.extension.tsx +27 -6
  209. package/src/metrics/metrics-cards/metrics-card.component.tsx +2 -1
  210. package/src/metrics/metrics-cards/providers-booked.extension.tsx +14 -6
  211. package/src/metrics/metrics-cards/scheduled-appointments.extension.tsx +20 -16
  212. package/src/metrics/metrics-container.component.tsx +1 -5
  213. package/src/metrics/metrics-header.component.tsx +2 -2
  214. package/src/patient-appointments/patient-appointments-detailed-summary.extension.tsx +1 -1
  215. package/src/patient-appointments/patient-upcoming-appointments-card.component.tsx +3 -4
  216. package/src/routes.json +2 -26
  217. package/src/store.ts +24 -41
  218. package/src/types/index.ts +7 -0
  219. package/src/workload/monthly-view-workload/monthly-view.component.tsx +2 -2
  220. package/src/workload/workload.component.tsx +3 -3
  221. package/src/workload/workload.resource.ts +1 -1
  222. package/translations/am.json +15 -6
  223. package/translations/ar.json +15 -6
  224. package/translations/ar_SY.json +15 -6
  225. package/translations/bn.json +15 -6
  226. package/translations/cs.json +15 -6
  227. package/translations/de.json +177 -168
  228. package/translations/en.json +15 -6
  229. package/translations/en_US.json +15 -6
  230. package/translations/es.json +15 -6
  231. package/translations/es_MX.json +15 -6
  232. package/translations/fr.json +15 -6
  233. package/translations/he.json +15 -6
  234. package/translations/hi.json +15 -6
  235. package/translations/hi_IN.json +15 -6
  236. package/translations/id.json +15 -6
  237. package/translations/it.json +15 -6
  238. package/translations/ka.json +15 -6
  239. package/translations/km.json +15 -6
  240. package/translations/ku.json +15 -6
  241. package/translations/ky.json +15 -6
  242. package/translations/lg.json +15 -6
  243. package/translations/ne.json +15 -6
  244. package/translations/pl.json +15 -6
  245. package/translations/pt.json +15 -6
  246. package/translations/pt_BR.json +15 -6
  247. package/translations/qu.json +15 -6
  248. package/translations/ro_RO.json +15 -6
  249. package/translations/ru_RU.json +15 -6
  250. package/translations/si.json +15 -6
  251. package/translations/sq.json +15 -6
  252. package/translations/sw.json +15 -6
  253. package/translations/sw_KE.json +15 -6
  254. package/translations/tr.json +15 -6
  255. package/translations/tr_TR.json +15 -6
  256. package/translations/uk.json +15 -6
  257. package/translations/uz.json +15 -6
  258. package/translations/uz@Latn.json +15 -6
  259. package/translations/uz_UZ.json +15 -6
  260. package/translations/vi.json +15 -6
  261. package/translations/zh.json +63 -54
  262. package/translations/zh_CN.json +19 -10
  263. package/translations/zh_TW.json +15 -6
  264. package/dist/1187.js +0 -1
  265. package/dist/1187.js.map +0 -1
  266. package/dist/126.js +0 -1
  267. package/dist/1499.js +0 -1
  268. package/dist/1499.js.map +0 -1
  269. package/dist/15.js +0 -1
  270. package/dist/1522.js +0 -1
  271. package/dist/1522.js.map +0 -1
  272. package/dist/1564.js +0 -1
  273. package/dist/1567.js +0 -1
  274. package/dist/1777.js +0 -1
  275. package/dist/1777.js.map +0 -1
  276. package/dist/1845.js +0 -1
  277. package/dist/1899.js +0 -1
  278. package/dist/1899.js.map +0 -1
  279. package/dist/1953.js +0 -1
  280. package/dist/2056.js +0 -11
  281. package/dist/2056.js.map +0 -1
  282. package/dist/2104.js +0 -1
  283. package/dist/2104.js.map +0 -1
  284. package/dist/215.js +0 -1
  285. package/dist/2178.js +0 -1
  286. package/dist/2417.js +0 -1
  287. package/dist/2417.js.map +0 -1
  288. package/dist/2566.js +0 -1
  289. package/dist/2586.js +0 -1
  290. package/dist/2586.js.map +0 -1
  291. package/dist/2759.js +0 -1
  292. package/dist/276.js +0 -1
  293. package/dist/276.js.map +0 -1
  294. package/dist/3089.js +0 -1
  295. package/dist/3089.js.map +0 -1
  296. package/dist/3127.js +0 -1
  297. package/dist/3127.js.map +0 -1
  298. package/dist/3230.js +0 -1
  299. package/dist/3277.js +0 -1
  300. package/dist/3277.js.map +0 -1
  301. package/dist/3441.js +0 -1
  302. package/dist/3565.js +0 -1
  303. package/dist/3571.js +0 -1
  304. package/dist/3571.js.map +0 -1
  305. package/dist/3746.js +0 -1
  306. package/dist/3925.js +0 -1
  307. package/dist/3946.js +0 -1
  308. package/dist/4085.js +0 -1
  309. package/dist/4085.js.map +0 -1
  310. package/dist/4108.js +0 -1
  311. package/dist/4108.js.map +0 -1
  312. package/dist/4448.js +0 -1
  313. package/dist/4448.js.map +0 -1
  314. package/dist/4744.js +0 -1
  315. package/dist/4744.js.map +0 -1
  316. package/dist/4809.js +0 -1
  317. package/dist/486.js +0 -1
  318. package/dist/486.js.map +0 -1
  319. package/dist/4894.js +0 -1
  320. package/dist/4970.js +0 -1
  321. package/dist/4970.js.map +0 -1
  322. package/dist/5130.js +0 -1
  323. package/dist/5187.js +0 -1
  324. package/dist/5218.js +0 -1
  325. package/dist/5218.js.map +0 -1
  326. package/dist/5327.js +0 -1
  327. package/dist/5327.js.map +0 -1
  328. package/dist/5388.js +0 -1
  329. package/dist/5388.js.map +0 -1
  330. package/dist/5491.js +0 -1
  331. package/dist/5491.js.map +0 -1
  332. package/dist/5595.js +0 -1
  333. package/dist/5657.js +0 -38
  334. package/dist/5657.js.map +0 -1
  335. package/dist/5961.js +0 -1
  336. package/dist/6133.js +0 -1
  337. package/dist/634.js +0 -1
  338. package/dist/634.js.map +0 -1
  339. package/dist/6456.js +0 -1
  340. package/dist/6466.js +0 -1
  341. package/dist/6613.js +0 -1
  342. package/dist/6783.js +0 -1
  343. package/dist/703.js +0 -1
  344. package/dist/703.js.map +0 -1
  345. package/dist/7251.js +0 -1
  346. package/dist/7251.js.map +0 -1
  347. package/dist/7348.js +0 -1
  348. package/dist/7433.js +0 -1
  349. package/dist/7433.js.map +0 -1
  350. package/dist/7513.js +0 -1
  351. package/dist/7513.js.map +0 -1
  352. package/dist/7543.js +0 -1
  353. package/dist/7607.js +0 -1
  354. package/dist/772.js +0 -1
  355. package/dist/8139.js +0 -1
  356. package/dist/8139.js.map +0 -1
  357. package/dist/8456.js +0 -1
  358. package/dist/8456.js.map +0 -1
  359. package/dist/8588.js +0 -1
  360. package/dist/8588.js.map +0 -1
  361. package/dist/8599.js +0 -1
  362. package/dist/8727.js +0 -1
  363. package/dist/8847.js +0 -1
  364. package/dist/8919.js +0 -1
  365. package/dist/8919.js.map +0 -1
  366. package/dist/9015.js +0 -1
  367. package/dist/9051.js +0 -1
  368. package/dist/9051.js.map +0 -1
  369. package/dist/906.js +0 -1
  370. package/dist/9065.js +0 -1
  371. package/dist/9182.js +0 -1
  372. package/dist/9260.js +0 -1
  373. package/dist/9260.js.map +0 -1
  374. package/dist/9327.js +0 -1
  375. package/dist/9327.js.map +0 -1
  376. package/dist/9339.js +0 -1
  377. package/dist/9453.js +0 -1
  378. package/dist/9589.js +0 -1
  379. package/dist/9589.js.map +0 -1
  380. package/dist/9650.js +0 -1
  381. package/dist/9650.js.map +0 -1
  382. package/dist/9833.js +0 -1
  383. package/dist/9833.js.map +0 -1
  384. package/dist/9920.js +0 -1
  385. package/dist/9938.js +0 -1
  386. package/dist/9943.js +0 -1
  387. package/dist/9943.js.map +0 -1
  388. package/src/appointments/scheduled/appointments-list.component.tsx +0 -51
  389. package/src/hooks/useClinicalMetrics.ts +0 -94
  390. package/src/hooks/useTodaysVisits.ts +0 -19
  391. package/src/scheduled-appointments-config-schema.ts +0 -177
@@ -1,19 +1,16 @@
1
1
  import React, { useEffect, useMemo, useState } from 'react';
2
2
  import { useTranslation } from 'react-i18next';
3
- import dayjs from 'dayjs';
4
- import isToday from 'dayjs/plugin/isToday';
5
- import utc from 'dayjs/plugin/utc';
6
3
  import {
7
4
  Button,
8
5
  DataTable,
9
6
  DataTableSkeleton,
10
7
  Layer,
8
+ MultiSelect,
11
9
  OverflowMenu,
12
10
  OverflowMenuItem,
13
11
  Pagination,
14
12
  Table,
15
13
  TableBatchAction,
16
- TableBatchActions,
17
14
  TableBody,
18
15
  TableCell,
19
16
  TableContainer,
@@ -33,7 +30,6 @@ import {
33
30
  import { Calendar, Download } from '@carbon/react/icons';
34
31
  import {
35
32
  ConfigurableLink,
36
- EmptyCard,
37
33
  formatDate,
38
34
  formatDatetime,
39
35
  isDesktop,
@@ -43,136 +39,126 @@ import {
43
39
  launchWorkspace2,
44
40
  usePagination,
45
41
  showModal,
42
+ TableBatchActions,
46
43
  } from '@openmrs/esm-framework';
47
44
  import { exportAppointmentsToSpreadsheet } from '../../helpers/excel';
48
- import { useTodaysVisits } from '../../hooks/useTodaysVisits';
49
- import { type Appointment } from '../../types';
45
+ import { AppointmentStatus, type Appointment } from '../../types';
50
46
  import { type ConfigObject } from '../../config-schema';
51
47
  import { getPageSizes, useAppointmentSearchResults } from '../utils';
52
- import { launchCreateAppointmentForm } from '../../helpers';
53
48
  import AppointmentActions from './appointments-actions.component';
54
49
  import AppointmentDetails from '../details/appointment-details.component';
50
+ import { useAppointmentsStore } from '../../store';
51
+ import { useActiveVisits } from '../../hooks/useActiveVisits';
55
52
  import styles from './appointments-table.scss';
56
53
 
57
- dayjs.extend(utc);
58
- dayjs.extend(isToday);
59
-
60
54
  interface AppointmentsTableProps {
61
55
  appointments: Array<Appointment>;
62
56
  isLoading: boolean;
63
57
  tableHeading: string;
64
- hasActiveFilters?: boolean;
65
58
  }
66
59
 
67
- const AppointmentsTable: React.FC<AppointmentsTableProps> = ({
68
- appointments,
69
- isLoading,
70
- tableHeading,
71
- hasActiveFilters,
72
- }) => {
60
+ const AppointmentsTable: React.FC<AppointmentsTableProps> = ({ appointments, isLoading, tableHeading }) => {
73
61
  const { t } = useTranslation();
74
62
  const [pageSize, setPageSize] = useState(25);
75
63
  const [searchString, setSearchString] = useState('');
64
+ const { selectedAppointmentStatuses, setSelectedAppointmentStatuses } = useAppointmentsStore();
76
65
  const config = useConfig<ConfigObject>();
77
66
  const { appointmentsTableColumns } = config;
78
- const searchResults = useAppointmentSearchResults(appointments, searchString);
67
+ const searchResults = useAppointmentSearchResults(appointments, searchString, selectedAppointmentStatuses);
79
68
  const { results, goTo, currentPage } = usePagination(searchResults, pageSize);
80
69
  const { customPatientChartUrl, patientIdentifierType } = useConfig<ConfigObject>();
81
- const { visits } = useTodaysVisits();
82
70
  const [selectedAppointmentUuids, setSelectedAppointmentUuids] = useState(new Set<string>());
83
71
  const layout = useLayoutType();
84
72
  const responsiveSize = isDesktop(layout) ? 'sm' : 'lg';
73
+ const { visits, mutateVisits } = useActiveVisits();
74
+
75
+ const activeVisitsByPatientUuid = useMemo(
76
+ () =>
77
+ new Map(
78
+ visits
79
+ ?.filter((visit) => visit?.startDatetime && !visit?.stopDatetime)
80
+ .map((visit) => [visit.patient.uuid, visit]),
81
+ ),
82
+ [visits],
83
+ );
85
84
 
86
85
  useEffect(() => {
87
86
  setSelectedAppointmentUuids(new Set());
88
87
  }, [appointments]);
89
88
 
89
+ const unsortableColumns = ['actions', 'overflowMenu'];
90
+
90
91
  const headerData = appointmentsTableColumns.map((columnKey) => ({
91
92
  header: t(columnKey, columnKey),
92
93
  key: columnKey,
93
94
  }));
95
+ headerData.push({ header: '', key: 'overflowMenu' });
94
96
 
95
97
  const rowData = useMemo(
96
98
  () =>
97
- results?.map((appointment) => ({
98
- id: appointment.uuid,
99
- patientName: (
100
- <ConfigurableLink
101
- className={styles.link}
102
- to={customPatientChartUrl}
103
- templateParams={{ patientUuid: appointment.patient.uuid }}>
104
- {appointment.patient.name}
105
- </ConfigurableLink>
106
- ),
107
- nextAppointmentDate: '--',
108
- identifier: patientIdentifierType
109
- ? (appointment.patient[patientIdentifierType.replaceAll(' ', '')] ?? appointment.patient.identifier)
110
- : appointment.patient.identifier,
111
- dateTime: formatDatetime(new Date(appointment.startDateTime)),
112
- serviceType: appointment.service.name,
113
- location: appointment.location?.name,
114
- provider: appointment.providers?.[0]?.name ?? '--',
115
- status: <AppointmentActions appointment={appointment} />,
116
- appointment,
117
- })),
118
- [results, customPatientChartUrl, patientIdentifierType],
99
+ results?.map((appointment) => {
100
+ return {
101
+ id: appointment.uuid,
102
+ patientName: (
103
+ <ConfigurableLink
104
+ className={styles.link}
105
+ to={customPatientChartUrl}
106
+ templateParams={{ patientUuid: appointment.patient.uuid }}>
107
+ {appointment.patient.name}
108
+ </ConfigurableLink>
109
+ ),
110
+ nextAppointmentDate: '--',
111
+ identifier: patientIdentifierType
112
+ ? (appointment.patient[patientIdentifierType.replaceAll(' ', '')] ?? appointment.patient.identifier)
113
+ : appointment.patient.identifier,
114
+ dateTime: formatDatetime(new Date(appointment.startDateTime)),
115
+ visitStartTime: activeVisitsByPatientUuid.has(appointment.patient.uuid)
116
+ ? formatDatetime(new Date(activeVisitsByPatientUuid.get(appointment.patient.uuid).startDatetime))
117
+ : '--',
118
+ serviceType: appointment.service.name,
119
+ location: appointment.location?.name,
120
+ provider: appointment.providers?.[0]?.name ?? '--',
121
+ status: t(appointment.status),
122
+ actions: (
123
+ <div className={styles.actionsCell}>
124
+ <AppointmentActions
125
+ appointment={appointment}
126
+ hasActiveVisit={activeVisitsByPatientUuid.has(appointment.patient.uuid)}
127
+ mutateVisits={mutateVisits}
128
+ />
129
+ </div>
130
+ ),
131
+ overflowMenu: (
132
+ <OverflowMenu align="left" aria-label={t('actions', 'Actions')} flipped size={responsiveSize}>
133
+ <OverflowMenuItem
134
+ className={styles.menuItem}
135
+ itemText={t('editAppointment', 'Edit appointment')}
136
+ onClick={() =>
137
+ launchWorkspace2('appointments-form-workspace', {
138
+ patientUuid: appointment.patient.uuid,
139
+ appointment: appointment,
140
+ })
141
+ }
142
+ />
143
+ </OverflowMenu>
144
+ ),
145
+ appointment,
146
+ };
147
+ }),
148
+ [results, customPatientChartUrl, patientIdentifierType, responsiveSize, t, activeVisitsByPatientUuid, mutateVisits],
119
149
  );
120
150
 
121
- const appointmentUuidsWithChangeableStatus = useMemo(() => {
122
- return appointments
123
- .filter((appointment) => {
124
- const visitDate = dayjs(appointment.startDateTime);
125
- const isFutureAppointment = visitDate.isAfter(dayjs());
126
- const isTodayAppointment = visitDate.isToday();
127
- const hasActiveVisitToday = visits?.some(
128
- (visit) => visit?.patient?.uuid === appointment.patient?.uuid && visit?.startDatetime,
129
- );
130
- return isFutureAppointment || (isTodayAppointment && !hasActiveVisitToday);
131
- })
132
- .map((appointment) => appointment.uuid);
133
- }, [appointments, visits]);
134
-
135
151
  if (isLoading) {
136
152
  return <DataTableSkeleton role="progressbar" rowCount={5} />;
137
153
  }
138
154
 
139
- if (hasActiveFilters && !appointments?.length) {
140
- return (
141
- <div className={styles.filterEmptyState}>
142
- <Layer level={0}>
143
- <Tile className={styles.filterEmptyStateTile}>
144
- <p className={styles.filterEmptyStateContent}>
145
- {t('noMatchingAppointments', 'No matching appointments found')}
146
- </p>
147
- <p className={styles.filterEmptyStateHelper}>{t('checkFilters', 'Check the filters above')}</p>
148
- </Tile>
149
- </Layer>
150
- </div>
151
- );
152
- }
153
-
154
- if (!appointments?.length) {
155
- const translatedHeading = t(tableHeading);
156
- return (
157
- <EmptyCard
158
- headerTitle={`${translatedHeading} ${t('appointments_lower', 'appointments')}`}
159
- displayText={
160
- tableHeading === t('todays', "Today's")
161
- ? t('appointmentsScheduledForToday', 'appointments scheduled for today')
162
- : t('appointments_lower', 'appointments')
163
- }
164
- launchForm={() => launchCreateAppointmentForm(t)}
165
- />
166
- );
167
- }
168
-
169
155
  return (
170
156
  <Layer className={styles.container}>
171
- <Tile className={styles.headerContainer}>
157
+ <div className={styles.headerContainer}>
172
158
  <div className={isDesktop(layout) ? styles.desktopHeading : styles.tabletHeading}>
173
- <h2>{`${t(tableHeading)} ${t('appointments', 'Appointments')}`}</h2>
159
+ <h2>{tableHeading}</h2>
174
160
  </div>
175
- </Tile>
161
+ </div>
176
162
  <DataTable
177
163
  aria-label={t('appointmentsTable', 'Appointments table')}
178
164
  data-floating-menu-container
@@ -222,6 +208,18 @@ const AppointmentsTable: React.FC<AppointmentsTableProps> = ({
222
208
  persistent
223
209
  size={responsiveSize}
224
210
  />
211
+ <MultiSelect
212
+ id="statusMultiSelect"
213
+ size={responsiveSize}
214
+ items={Object.values(AppointmentStatus).map((status) => ({ id: status, label: t(status) }))}
215
+ itemToString={(item) => (item ? item.label : '')}
216
+ label={t('filterAppointmentsByStatus', 'Filter appointments by status')}
217
+ onChange={({ selectedItems }) =>
218
+ setSelectedAppointmentStatuses([...new Set(selectedItems.map((item) => item.id))])
219
+ }
220
+ type="inline"
221
+ selectedItems={selectedAppointmentStatuses.map((status) => ({ id: status, label: t(status) }))}
222
+ />
225
223
  <Button
226
224
  size={responsiveSize}
227
225
  kind="tertiary"
@@ -245,22 +243,20 @@ const AppointmentsTable: React.FC<AppointmentsTableProps> = ({
245
243
  <TableExpandHeader enableToggle {...getExpandHeaderProps()} />
246
244
  <TableSelectAll
247
245
  {...getSelectionProps()}
248
- checked={
249
- selectedAppointmentUuids.size === appointmentUuidsWithChangeableStatus.length &&
250
- selectedAppointmentUuids.size > 0
251
- }
246
+ checked={selectedAppointmentUuids.size === rows.length && selectedAppointmentUuids.size > 0}
252
247
  onSelect={() => {
253
- if (selectedAppointmentUuids.size < appointmentUuidsWithChangeableStatus.length) {
254
- setSelectedAppointmentUuids(new Set(appointmentUuidsWithChangeableStatus));
248
+ if (selectedAppointmentUuids.size < rows.length) {
249
+ setSelectedAppointmentUuids(new Set(rows.map((row) => row.id)));
255
250
  } else {
256
251
  setSelectedAppointmentUuids(new Set());
257
252
  }
258
253
  }}
259
254
  />
260
255
  {headers.map((header) => (
261
- <TableHeader {...getHeaderProps({ header })}>{header.header}</TableHeader>
256
+ <TableHeader {...getHeaderProps({ header, isSortable: !unsortableColumns.includes(header.key) })}>
257
+ {header.header}
258
+ </TableHeader>
262
259
  ))}
263
- <TableHeader aria-label={t('actions', 'Actions')} />
264
260
  </TableRow>
265
261
  </TableHead>
266
262
  <TableBody>
@@ -271,15 +267,12 @@ const AppointmentsTable: React.FC<AppointmentsTableProps> = ({
271
267
  return null;
272
268
  }
273
269
 
274
- const canChangeStatus = appointmentUuidsWithChangeableStatus.includes(matchingAppointment.uuid);
275
-
276
270
  return (
277
271
  <React.Fragment key={row.id}>
278
272
  <TableExpandRow {...getRowProps({ row })}>
279
273
  <TableSelectRow
280
274
  {...getSelectionProps({ row })}
281
275
  checked={selectedAppointmentUuids.has(row.id)}
282
- disabled={!canChangeStatus}
283
276
  onSelect={() => {
284
277
  if (selectedAppointmentUuids.has(row.id)) {
285
278
  setSelectedAppointmentUuids(
@@ -293,26 +286,6 @@ const AppointmentsTable: React.FC<AppointmentsTableProps> = ({
293
286
  {row.cells.map((cell) => (
294
287
  <TableCell key={cell.id}>{cell.value?.content ?? cell.value}</TableCell>
295
288
  ))}
296
- <TableCell className="cds--table-column-menu">
297
- {canChangeStatus ? (
298
- <OverflowMenu
299
- align="left"
300
- aria-label={t('actions', 'Actions')}
301
- flipped
302
- size={responsiveSize}>
303
- <OverflowMenuItem
304
- className={styles.menuItem}
305
- itemText={t('editAppointment', 'Edit appointment')}
306
- onClick={() =>
307
- launchWorkspace2('appointments-form-workspace', {
308
- patientUuid: matchingAppointment.patient.uuid,
309
- appointment: matchingAppointment,
310
- })
311
- }
312
- />
313
- </OverflowMenu>
314
- ) : null}
315
- </TableCell>
316
289
  </TableExpandRow>
317
290
  {row.isExpanded ? (
318
291
  <TableExpandedRow className={styles.expandedRow} colSpan={headers.length + 2}>
@@ -338,24 +311,25 @@ const AppointmentsTable: React.FC<AppointmentsTableProps> = ({
338
311
  </Tile>
339
312
  </Layer>
340
313
  </div>
341
- ) : null}
314
+ ) : (
315
+ <Pagination
316
+ backwardText={t('previousPage', 'Previous page')}
317
+ forwardText={t('nextPage', 'Next page')}
318
+ itemsPerPageText={t('itemsPerPage', 'Items per page') + ':'}
319
+ page={currentPage}
320
+ pageNumberText={t('pageNumber', 'Page number')}
321
+ pageSize={pageSize}
322
+ pageSizes={getPageSizes(appointments, pageSize) ?? []}
323
+ onChange={({ page, pageSize }) => {
324
+ goTo(page);
325
+ setPageSize(pageSize);
326
+ }}
327
+ totalItems={appointments.length ?? 0}
328
+ />
329
+ )}
342
330
  </>
343
331
  )}
344
332
  </DataTable>
345
- <Pagination
346
- backwardText={t('previousPage', 'Previous page')}
347
- forwardText={t('nextPage', 'Next page')}
348
- itemsPerPageText={t('itemsPerPage', 'Items per page') + ':'}
349
- page={currentPage}
350
- pageNumberText={t('pageNumber', 'Page number')}
351
- pageSize={pageSize}
352
- pageSizes={getPageSizes(appointments, pageSize) ?? []}
353
- onChange={({ page, pageSize }) => {
354
- goTo(page);
355
- setPageSize(pageSize);
356
- }}
357
- totalItems={appointments.length ?? 0}
358
- />
359
333
  </Layer>
360
334
  );
361
335
  };
@@ -20,7 +20,8 @@
20
20
  }
21
21
 
22
22
  .container {
23
- border: 1px solid colors.$gray-20;
23
+ background: colors.$gray-10;
24
+ padding: layout.$spacing-05;
24
25
 
25
26
  :global(.cds--table-toolbar) {
26
27
  position: relative;
@@ -32,8 +33,8 @@
32
33
  display: flex;
33
34
  justify-content: space-between;
34
35
  align-items: center;
35
- padding: layout.$spacing-04 0 layout.$spacing-04 layout.$spacing-05;
36
- background-color: colors.$white-0;
36
+ padding: layout.$spacing-04 0;
37
+ background-color: colors.$gray-10;
37
38
  }
38
39
 
39
40
  .tabletHeading {
@@ -48,7 +49,6 @@
48
49
  display: flex;
49
50
  justify-content: space-between;
50
51
  text-align: left;
51
- text-transform: capitalize;
52
52
 
53
53
  h2 {
54
54
  @include type.type-style('heading-compact-02');
@@ -73,7 +73,7 @@
73
73
 
74
74
  .searchbar {
75
75
  input {
76
- background-color: colors.$gray-10;
76
+ background-color: $ui-02 !important;
77
77
  }
78
78
  }
79
79
 
@@ -92,6 +92,11 @@
92
92
  max-width: none;
93
93
  }
94
94
 
95
+ .actionsCell {
96
+ display: flex;
97
+ align-items: center;
98
+ }
99
+
95
100
  .content {
96
101
  @include type.type-style('heading-compact-02');
97
102
  color: $text-02;
@@ -126,7 +131,6 @@
126
131
  // Overriding styles for RTL support
127
132
  html[dir='rtl'] {
128
133
  .headerContainer {
129
- padding: layout.$spacing-04 layout.$spacing-05 layout.$spacing-04 0;
130
134
  svg {
131
135
  margin-left: 0;
132
136
  margin-right: layout.$spacing-03;
@@ -76,17 +76,14 @@ describe('AppointmentsTable', () => {
76
76
  mockUseConfig.mockReturnValue({
77
77
  ...getDefaultsFromConfigSchema(configSchema),
78
78
  customPatientChartUrl: 'url-to-patient-chart',
79
- checkInButton: { enabled: false, showIfActiveVisit: false, customUrl: null },
79
+ checkInButton: { enabled: false, customUrl: null },
80
80
  checkOutButton: { enabled: false, customUrl: null },
81
81
  });
82
82
  });
83
83
 
84
84
  it('renders an empty state if appointments data is unavailable', async () => {
85
85
  renderAppointmentsTable();
86
-
87
- await screen.findByRole('heading', { name: /scheduled appointment/i });
88
-
89
- expect(getByTextWithMarkup('There are no appointments to display')).toBeInTheDocument();
86
+ expect(getByTextWithMarkup('No appointments to display')).toBeInTheDocument();
90
87
  });
91
88
 
92
89
  it('renders a loading state when fetching data', () => {
@@ -98,7 +95,6 @@ describe('AppointmentsTable', () => {
98
95
  it('renders a tabular overview of the scheduled appointments', async () => {
99
96
  renderAppointmentsTable({ appointments: mockAppointments });
100
97
 
101
- await screen.findByRole('heading', { name: /scheduled appointment/i });
102
98
  expect(screen.getByRole('search', { name: /filter table/i })).toBeInTheDocument();
103
99
  expect(screen.getByRole('button', { name: /download/i })).toBeInTheDocument();
104
100
  expect(screen.getByRole('row', { name: /john wilson 100gej hiv clinic outpatient/i })).toBeInTheDocument();
@@ -110,8 +106,6 @@ describe('AppointmentsTable', () => {
110
106
  const user = userEvent.setup();
111
107
 
112
108
  renderAppointmentsTable({ appointments: mockAppointments });
113
-
114
- await screen.findByRole('heading', { name: /scheduled appointment/i });
115
109
  const searchInput = screen.getByRole('searchbox');
116
110
  await user.type(searchInput, 'John');
117
111
  expect(searchInput).toHaveValue('John');
@@ -122,7 +116,6 @@ describe('AppointmentsTable', () => {
122
116
 
123
117
  renderAppointmentsTable({ appointments: mockAppointments });
124
118
 
125
- await screen.findByRole('heading', { name: /scheduled appointment/i });
126
119
  const downloadButton = screen.getByRole('button', { name: /download/i });
127
120
  await user.click(downloadButton);
128
121
  expect(downloadButton).toBeInTheDocument();
@@ -24,7 +24,7 @@ import { changeAppointmentStatus } from '../../patient-appointments/patient-appo
24
24
  import { getActiveVisitsForPatient } from './batch-change-appointment-statuses.resources';
25
25
  import { type Appointment, AppointmentStatus } from '../../types';
26
26
  import { type ConfigObject } from '../../config-schema';
27
- import { useMutateAppointments } from '../../form/appointments-form.resource';
27
+ import { useMutateAppointments } from '../../hooks/useMutateAppointments';
28
28
  import styles from './batch-change-appointment-statuses.scss';
29
29
 
30
30
  interface BatchChangeAppointmentStatusesModalProps {
@@ -3,7 +3,7 @@ import { render, screen } from '@testing-library/react';
3
3
  import userEvent from '@testing-library/user-event';
4
4
  import { getDefaultsFromConfigSchema, showSnackbar, useConfig } from '@openmrs/esm-framework';
5
5
  import { changeAppointmentStatus } from '../../patient-appointments/patient-appointments.resource';
6
- import { useMutateAppointments } from '../../form/appointments-form.resource';
6
+ import { useMutateAppointments } from '../../hooks/useMutateAppointments';
7
7
  import { type Appointment, AppointmentKind, AppointmentStatus } from '../../types';
8
8
  import { type ConfigObject, configSchema } from '../../config-schema';
9
9
  import BatchChangeAppointmentStatusesModal from './batch-change-appointment-statuses.modal';
@@ -19,14 +19,14 @@ jest.mock('../../patient-appointments/patient-appointments.resource', () => ({
19
19
  changeAppointmentStatus: jest.fn(),
20
20
  }));
21
21
 
22
- jest.mock('../../form/appointments-form.resource', () => ({
23
- useMutateAppointments: jest.fn(),
24
- }));
25
-
26
22
  jest.mock('./batch-change-appointment-statuses.resources', () => ({
27
23
  getActiveVisitsForPatient: jest.fn(),
28
24
  }));
29
25
 
26
+ jest.mock('../../hooks/useMutateAppointments', () => ({
27
+ useMutateAppointments: jest.fn(),
28
+ }));
29
+
30
30
  const mockAppointment1: Appointment = {
31
31
  uuid: 'appointment-1',
32
32
  appointmentNumber: '0001',
@@ -86,11 +86,13 @@ const mockAppointment2: Appointment = {
86
86
  describe('BatchChangeAppointmentStatusesModal', () => {
87
87
  beforeEach(() => {
88
88
  jest.clearAllMocks();
89
- mockUseMutateAppointments.mockReturnValue({ mutateAppointments: mockMutateAppointments });
90
89
  mockUseConfig.mockReturnValue({
91
90
  ...getDefaultsFromConfigSchema(configSchema),
92
91
  checkOutButton: { enabled: true, customUrl: '' },
93
92
  });
93
+ mockUseMutateAppointments.mockReturnValue({
94
+ mutateAppointments: mockMutateAppointments,
95
+ });
94
96
  });
95
97
 
96
98
  it('renders the modal with appointment list', () => {
@@ -2,45 +2,76 @@ import React from 'react';
2
2
  import { Button } from '@carbon/react';
3
3
  import { useTranslation } from 'react-i18next';
4
4
  import { type Appointment } from '../../types';
5
- import dayjs from 'dayjs';
6
- import isToday from 'dayjs/plugin/isToday';
7
- import utc from 'dayjs/plugin/utc';
8
- import { navigate, useConfig, launchWorkspace2 } from '@openmrs/esm-framework';
5
+ import { navigate, useConfig, launchWorkspace2, showSnackbar } from '@openmrs/esm-framework';
6
+ import { changeAppointmentStatus } from '../../patient-appointments/patient-appointments.resource';
7
+ import { useMutateAppointments } from '../../hooks/useMutateAppointments';
9
8
  import { type ConfigObject } from '../../config-schema';
10
- dayjs.extend(utc);
11
- dayjs.extend(isToday);
12
9
 
13
10
  interface CheckInButtonProps {
14
11
  patientUuid: string;
15
12
  appointment: Appointment;
13
+ hasActiveVisit?: boolean;
14
+ mutateVisits: () => void;
16
15
  }
17
16
 
18
- const CheckInButton: React.FC<CheckInButtonProps> = ({ appointment, patientUuid }) => {
17
+ const CheckInButton: React.FC<CheckInButtonProps> = ({ appointment, patientUuid, hasActiveVisit, mutateVisits }) => {
19
18
  const { checkInButton } = useConfig<ConfigObject>();
20
19
  const { t } = useTranslation();
20
+ const { mutateAppointments } = useMutateAppointments();
21
+
21
22
  return (
22
- <>
23
- {checkInButton.enabled &&
24
- (dayjs(appointment.startDateTime).isAfter(dayjs()) || dayjs(appointment.startDateTime).isToday()) && (
25
- <Button
26
- size="sm"
27
- kind="tertiary"
28
- onClick={() =>
29
- checkInButton.customUrl
30
- ? navigate({
31
- to: checkInButton.customUrl,
32
- templateParams: { patientUuid: appointment.patient.uuid, appointmentUuid: appointment.uuid },
33
- })
34
- : launchWorkspace2('appointments-start-visit-workspace', {
35
- patientUuid: patientUuid,
36
- showPatientHeader: true,
37
- openedFrom: 'appointments-check-in',
38
- })
39
- }>
40
- {t('checkIn', 'Check in')}
41
- </Button>
42
- )}
43
- </>
23
+ <Button
24
+ size="sm"
25
+ kind="tertiary"
26
+ onClick={() => {
27
+ // customUrl always takes priority — operator-configured override for entire check-in flow
28
+ if (checkInButton.customUrl) {
29
+ navigate({
30
+ to: checkInButton.customUrl,
31
+ templateParams: { patientUuid: appointment.patient.uuid, appointmentUuid: appointment.uuid },
32
+ });
33
+ return;
34
+ }
35
+
36
+ // Patient already has an active visit — only update appointment status, do not start a new visit
37
+ if (hasActiveVisit) {
38
+ changeAppointmentStatus('CheckedIn', appointment.uuid)
39
+ .then(() => {
40
+ showSnackbar({
41
+ title: t('checkedIn', 'Checked in'),
42
+ subtitle: t(
43
+ 'appointmentCheckedInWithExistingVisit',
44
+ 'Appointment checked in using existing active visit',
45
+ ),
46
+ kind: 'success',
47
+ isLowContrast: true,
48
+ });
49
+ mutateAppointments?.();
50
+ })
51
+ .catch((error) => {
52
+ console.error('Check-in failed:', error);
53
+ showSnackbar({
54
+ title: t('checkInFailed', 'Check-in failed'),
55
+ subtitle:
56
+ error?.message ??
57
+ t('appointmentCheckInFailed', 'An error occurred while checking in the appointment'),
58
+ kind: 'error',
59
+ isLowContrast: false,
60
+ });
61
+ });
62
+ return;
63
+ }
64
+
65
+ // No active visit, no customUrl — launch default start visit workspace
66
+ launchWorkspace2('appointments-start-visit-workspace', {
67
+ patientUuid,
68
+ showPatientHeader: true,
69
+ openedFrom: 'appointments-check-in',
70
+ onVisitStarted: mutateVisits,
71
+ });
72
+ }}>
73
+ {t('checkIn', 'Check in')}
74
+ </Button>
44
75
  );
45
76
  };
46
77
 
@@ -3,7 +3,7 @@ import { useTranslation } from 'react-i18next';
3
3
  import { Button, ModalBody, ModalFooter, ModalHeader } from '@carbon/react';
4
4
  import { showSnackbar, updateVisit, useVisit } from '@openmrs/esm-framework';
5
5
  import { changeAppointmentStatus } from '../../patient-appointments/patient-appointments.resource';
6
- import { useMutateAppointments } from '../../form/appointments-form.resource';
6
+ import { useMutateAppointments } from '../../hooks/useMutateAppointments';
7
7
 
8
8
  interface EndAppointmentModalProps {
9
9
  patientUuid: string;
@@ -20,7 +20,7 @@ jest.mock('../../patient-appointments/patient-appointments.resource', () => ({
20
20
  changeAppointmentStatus: jest.fn().mockResolvedValue({}),
21
21
  }));
22
22
 
23
- jest.mock('../../form/appointments-form.resource', () => ({
23
+ jest.mock('../../hooks/useMutateAppointments', () => ({
24
24
  useMutateAppointments: jest.fn().mockReturnValue({ mutateAppointments: jest.fn() }),
25
25
  }));
26
26