@kenyaemr/esm-express-workflow-app 5.4.3 → 5.4.4-pre.10

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.
@@ -11,8 +11,7 @@ import {
11
11
  TableHeader,
12
12
  TableRow,
13
13
  } from '@carbon/react';
14
- import { ConfigurableLink, showModal, useConfig, useDebounce, usePagination } from '@openmrs/esm-framework';
15
- import { usePaginationInfo } from '@openmrs/esm-patient-common-lib';
14
+ import { ConfigurableLink, showModal, useConfig, useDebounce } from '@openmrs/esm-framework';
16
15
  import dayjs from 'dayjs';
17
16
  import relativeTime from 'dayjs/plugin/relativeTime';
18
17
  import startCase from 'lodash-es/startCase';
@@ -24,8 +23,9 @@ import lowerCase from 'lodash-es/lowerCase';
24
23
  import { type ExpressWorkflowConfig } from '../../../config-schema';
25
24
  import { spaBasePath } from '../../../constants';
26
25
  import { serveQueueEntry } from '../../../hooks/useServiceQueues';
27
- import { type QueueEntry } from '../../../types/index';
26
+ import { type QueueEntriesPagination, type QueueEntry } from '../../../types/index';
28
27
  import styles from './queue-entry-table.scss';
28
+ import { usePaginationInfo } from '@openmrs/esm-patient-common-lib/src';
29
29
 
30
30
  // Extend dayjs with the relativeTime plugin
31
31
  dayjs.extend(relativeTime);
@@ -34,16 +34,19 @@ type QueueEntryTableProps = {
34
34
  navigatePath?: string;
35
35
  queueEntries: Array<QueueEntry>;
36
36
  usePatientChart?: boolean;
37
+ pagination?: QueueEntriesPagination;
38
+ onPageSizeChange?: (pageSize: number) => void;
37
39
  };
38
40
 
39
41
  const QueueEntryTable: React.FC<QueueEntryTableProps> = ({
40
42
  navigatePath = 'triage',
41
43
  queueEntries,
42
44
  usePatientChart,
45
+ pagination,
46
+ onPageSizeChange,
43
47
  }) => {
44
48
  const { visitQueueNumberAttributeUuid } = useConfig<ExpressWorkflowConfig>();
45
49
  const [searchString, setSearchString] = useState('');
46
- const pageSize = 10;
47
50
  const { t } = useTranslation();
48
51
  const debouncedSearchString = useDebounce(searchString, 500);
49
52
  const filteredQueueEntries = useMemo(() => {
@@ -51,48 +54,32 @@ const QueueEntryTable: React.FC<QueueEntryTableProps> = ({
51
54
  return queueEntry.patient.person.display.toLowerCase().includes(debouncedSearchString.toLowerCase());
52
55
  });
53
56
  }, [queueEntries, debouncedSearchString]);
54
- const { currentPage, goTo, results } = usePagination(filteredQueueEntries, pageSize);
55
- const { pageSizes } = usePaginationInfo(pageSize, queueEntries.length, currentPage, results.length);
56
57
 
57
- const headers = [
58
- {
59
- header: t('name', 'Name'),
60
- key: 'patientName',
61
- },
62
- {
63
- header: t('queueNumber', 'Queue Number'),
64
- key: 'queueNumber',
65
- },
66
- {
67
- header: t('comingFrom', 'Coming from'),
68
- key: 'previousQueue',
69
- },
70
- {
71
- header: t('priority', 'Priority'),
72
- key: 'priority',
73
- },
74
- {
75
- header: t('priorityComment', 'Priority Comment'),
76
- key: 'priorityComment',
77
- },
78
- {
79
- header: t('status', 'status'),
80
- key: 'status',
81
- },
82
- {
83
- header: t('queue', 'Queue'),
84
- key: 'queue',
85
- },
86
- {
87
- header: t('waitTime', 'Wait time'),
88
- key: 'waitTime',
89
- },
90
- ];
58
+ const { pageSizes } = usePaginationInfo(
59
+ pagination.currentPageSize.current,
60
+ pagination.totalCount,
61
+ pagination.currentPage,
62
+ filteredQueueEntries.length,
63
+ );
64
+
65
+ const headers = useMemo(
66
+ () => [
67
+ { header: t('name', 'Name'), key: 'patientName' },
68
+ { header: t('queueNumber', 'Queue Number'), key: 'queueNumber' },
69
+ { header: t('comingFrom', 'Coming from'), key: 'previousQueue' },
70
+ { header: t('priority', 'Priority'), key: 'priority' },
71
+ { header: t('priorityComment', 'Priority Comment'), key: 'priorityComment' },
72
+ { header: t('status', 'status'), key: 'status' },
73
+ { header: t('queue', 'Queue'), key: 'queue' },
74
+ { header: t('waitTime', 'Wait time'), key: 'waitTime' },
75
+ ],
76
+ [t],
77
+ );
91
78
 
92
79
  const handleCallQueueEntry = async (queueEntry: QueueEntry) => {
93
- const queueNumber = queueEntry.visit.attributes?.filter(
80
+ const queueNumber = queueEntry.visit.attributes?.find(
94
81
  (attr) => attr['attributeType']?.uuid === visitQueueNumberAttributeUuid,
95
- )?.[0];
82
+ );
96
83
  const response = await serveQueueEntry(
97
84
  queueEntry.queue.name,
98
85
  queueNumber.value,
@@ -108,35 +95,37 @@ const QueueEntryTable: React.FC<QueueEntryTableProps> = ({
108
95
  }
109
96
  };
110
97
 
111
- const rows = results?.map((queueEntry) => {
112
- const visitNumber = queueEntry?.visit?.attributes?.find(
113
- (attr) => attr?.attributeType?.uuid === visitQueueNumberAttributeUuid,
114
- );
98
+ const rows = useMemo(() => {
99
+ return filteredQueueEntries?.map((queueEntry) => {
100
+ const visitNumber = queueEntry?.visit?.attributes?.find(
101
+ (attr) => attr?.attributeType?.uuid === visitQueueNumberAttributeUuid,
102
+ );
115
103
 
116
- const patientChartUrl = usePatientChart
117
- ? `${window.spaBase}/patient/${queueEntry.patient.uuid}/chart/Patient Summary?path=${navigatePath}`
118
- : `${spaBasePath}/${navigatePath}/${queueEntry.patient.uuid}`;
104
+ const patientChartUrl = usePatientChart
105
+ ? `${globalThis.spaBase}/patient/${queueEntry.patient.uuid}/chart/Patient Summary?path=${navigatePath}`
106
+ : `${spaBasePath}/${navigatePath}/${queueEntry.patient.uuid}`;
119
107
 
120
- return {
121
- id: queueEntry.uuid,
122
- queueNumber: visitNumber?.value ?? '--',
123
- previousQueue: startCase(queueEntry.previousQueueEntry?.queue?.display?.toLowerCase() ?? '--'),
124
- patientName: (
125
- <ConfigurableLink className={styles.link} to={patientChartUrl}>
126
- {startCase(queueEntry.patient.person.display.toLowerCase())}
127
- </ConfigurableLink>
128
- ),
129
- priority: (
130
- <div className={styles.priorityPill} data-priority={lowerCase(queueEntry?.priority?.display)}>
131
- {t(queueEntry?.priority?.display, capitalize(queueEntry?.priority?.display.replace('_', ' ')))}
132
- </div>
133
- ),
134
- priorityComment: startCase(queueEntry.priorityComment?.toLowerCase() ?? '--'),
135
- status: queueEntry?.status?.display ?? '--',
136
- queue: startCase(queueEntry?.queue?.display?.toLowerCase() ?? '--'),
137
- waitTime: dayjs(queueEntry.startedAt).fromNow(),
138
- };
139
- });
108
+ return {
109
+ id: queueEntry.uuid,
110
+ queueNumber: visitNumber?.value ?? '--',
111
+ previousQueue: startCase(queueEntry.previousQueueEntry?.queue?.display?.toLowerCase() ?? '--'),
112
+ patientName: (
113
+ <ConfigurableLink className={styles.link} to={patientChartUrl}>
114
+ {startCase(queueEntry.patient.person.display.toLowerCase())}
115
+ </ConfigurableLink>
116
+ ),
117
+ priority: (
118
+ <div className={styles.priorityPill} data-priority={lowerCase(queueEntry?.priority?.display)}>
119
+ {t(queueEntry?.priority?.display, capitalize(queueEntry?.priority?.display.replace('_', ' ')))}
120
+ </div>
121
+ ),
122
+ priorityComment: startCase(queueEntry.priorityComment?.toLowerCase() ?? '--'),
123
+ status: queueEntry?.status?.display ?? '--',
124
+ queue: startCase(queueEntry?.queue?.display?.toLowerCase() ?? '--'),
125
+ waitTime: dayjs(queueEntry.startedAt).fromNow(),
126
+ };
127
+ });
128
+ }, [filteredQueueEntries, visitQueueNumberAttributeUuid, navigatePath, usePatientChart, t]);
140
129
 
141
130
  if (queueEntries.length === 0) {
142
131
  return <div>{t('noPatientsAwaiting', 'No patients awaiting service')}</div>;
@@ -172,7 +161,7 @@ const QueueEntryTable: React.FC<QueueEntryTableProps> = ({
172
161
  <TableCell className="cds--table-column-menu">
173
162
  <OverflowMenu size="sm" aria-label="overflow-menu" flipped align="right">
174
163
  <OverflowMenuItem
175
- onClick={() => handleCallQueueEntry(results[index])}
164
+ onClick={() => handleCallQueueEntry(filteredQueueEntries[index])}
176
165
  itemText={t('call', 'Call')}
177
166
  />
178
167
  </OverflowMenu>
@@ -183,17 +172,20 @@ const QueueEntryTable: React.FC<QueueEntryTableProps> = ({
183
172
  </Table>
184
173
  )}
185
174
  </DataTable>
186
- <Pagination
187
- pageSizes={pageSizes}
188
- forwardText={t('nextPage', 'Next page')}
189
- backwardText={t('previousPage', 'Previous page')}
190
- page={currentPage}
191
- pageSize={pageSize}
192
- totalItems={filteredQueueEntries.length}
193
- onChange={({ page }) => {
194
- goTo(page);
195
- }}
196
- />
175
+ {pagination && (
176
+ <Pagination
177
+ pageSizes={pageSizes}
178
+ forwardText={t('nextPage', 'Next page')}
179
+ backwardText={t('previousPage', 'Previous page')}
180
+ page={pagination.currentPage}
181
+ pageSize={pagination.currentPageSize.current}
182
+ totalItems={pagination.totalCount}
183
+ onChange={({ page, pageSize }) => {
184
+ pagination.goTo(page);
185
+ onPageSizeChange?.(pageSize as number);
186
+ }}
187
+ />
188
+ )}
197
189
  </div>
198
190
  );
199
191
  };
@@ -0,0 +1,32 @@
1
+ import React from 'react';
2
+ import Card from '../cards/card.component';
3
+ import styles from './queue-tab.scss';
4
+
5
+ export type QueueSummaryCard = {
6
+ title: string;
7
+ value: string;
8
+ categories?: Array<{ label: string; value: number; onClick?: () => void }>;
9
+ onClick?: () => void;
10
+ refreshButton?: React.ReactNode;
11
+ };
12
+
13
+ interface QueueSummaryCardsProps {
14
+ cards?: Array<QueueSummaryCard>;
15
+ }
16
+
17
+ const QueueSummaryCards: React.FC<QueueSummaryCardsProps> = ({ cards }) => (
18
+ <div className={styles.cards}>
19
+ {cards?.map((card) => (
20
+ <Card
21
+ key={card.title}
22
+ title={card.title}
23
+ total={card.value}
24
+ categories={card.categories}
25
+ onClick={card.onClick}
26
+ refreshButton={card.refreshButton}
27
+ />
28
+ ))}
29
+ </div>
30
+ );
31
+
32
+ export default QueueSummaryCards;
@@ -1,23 +1,17 @@
1
- import { Tabs, TabList, Tab, TabPanels, TabPanel, InlineLoading, TabsSkeleton } from '@carbon/react';
2
- import React, { useState, useEffect, useMemo, useCallback } from 'react';
1
+ import React, { useState, useMemo, useCallback } from 'react';
2
+ import dayjs from 'dayjs';
3
3
  import { useTranslation } from 'react-i18next';
4
+ import { Tabs, TabList, Tab, TabPanels, TabPanel, InlineLoading, TabsSkeleton } from '@carbon/react';
4
5
  import startCase from 'lodash-es/startCase';
5
- import Card from '../cards/card.component';
6
- import { Queue, QueueFilter } from '../../types/index';
7
- import QueueEntryTable from './queue-entry/queue-entry-table.component';
6
+
7
+ import FiltersHeader from './filters-header.component';
8
+ import { type Queue, type QueueFilter } from '../../types/index';
8
9
  import { useQueueEntries } from '../../hooks/useServiceQueues';
10
+ import QueueEntryTable from './queue-entry/queue-entry-table.component';
9
11
  import styles from './queue-tab.scss';
10
- import FiltersHeader from './filters-header.component';
11
12
 
12
13
  type QueueTabProps = {
13
14
  queues: Array<Queue>;
14
- cards?: Array<{
15
- title: string;
16
- value: string;
17
- categories?: Array<{ label: string; value: number; onClick?: () => void }>;
18
- onClick?: () => void;
19
- refreshButton?: React.ReactNode; // Add this property
20
- }>;
21
15
  navigatePath: string;
22
16
  onTabChanged?: (queue: Queue) => void;
23
17
  usePatientChart?: boolean;
@@ -25,9 +19,10 @@ type QueueTabProps = {
25
19
  onFiltersChanged?: (filters: Array<QueueFilter>) => void;
26
20
  };
27
21
 
22
+ const startedOnOrAfter = dayjs().subtract(24, 'hour').format('YYYY-MM-DD HH:mm:ss');
23
+
28
24
  const QueueTab: React.FC<QueueTabProps> = ({
29
25
  queues,
30
- cards,
31
26
  navigatePath,
32
27
  onTabChanged,
33
28
  usePatientChart,
@@ -37,43 +32,49 @@ const QueueTab: React.FC<QueueTabProps> = ({
37
32
  const { t } = useTranslation();
38
33
 
39
34
  // Filter queues with rooms first
35
+ const [pageSize, setPageSize] = useState(100);
40
36
  const validQueues = useMemo(() => queues.filter((queue) => queue?.queueRooms?.length > 0), [queues]);
41
37
 
42
38
  // Set initial selected queue to first valid queue
43
39
  const [selectedTabIndex, setSelectedTabIndex] = useState(0);
44
- const [isInitialLoad, setIsInitialLoad] = useState(true);
45
40
 
46
41
  const selectedQueue = validQueues[selectedTabIndex];
47
42
 
48
- const { queueEntries, isLoading, error } = useQueueEntries({
49
- location: selectedQueue?.location?.uuid ? [selectedQueue.location.uuid] : undefined,
50
- statuses: filters?.filter((filter) => filter.key === 'status')?.flatMap((filter) => filter.value.split(',')),
51
- });
52
-
53
- useEffect(() => {
54
- if (!isLoading && isInitialLoad) {
55
- setIsInitialLoad(false);
56
- }
57
- }, [isLoading, isInitialLoad]);
43
+ const {
44
+ queueEntries,
45
+ isLoading: isLoadingQueueEntries,
46
+ isValidating: isValidatingQueueEntries,
47
+ error,
48
+ pagination,
49
+ } = useQueueEntries(
50
+ {
51
+ location: selectedQueue?.location?.uuid ? [selectedQueue.location.uuid] : undefined,
52
+ startedOnOrAfter: startedOnOrAfter,
53
+ },
54
+ pageSize,
55
+ );
58
56
 
59
57
  const queueEntriesByService = useMemo(() => {
60
58
  if (!queueEntries?.length || !selectedQueue?.uuid) {
61
59
  return [];
62
60
  }
63
61
  const priorityFilter = filters?.find((filter) => filter.key === 'priority')?.value;
62
+ const statusFilter =
63
+ filters?.filter((filter) => filter.key === 'status')?.flatMap((filter) => filter.value.split(',')) ?? [];
64
64
  const serviceAwaitingFilter = filters?.find((filter) => filter.key === 'service_awaiting')?.value?.split(',') ?? []; // Patient UUIDs
65
65
  const serviceCompletedFilter =
66
66
  filters?.find((filter) => filter.key === 'service_completed')?.value?.split(',') ?? []; // Patient UUIDs
67
67
  const filtered = queueEntries.filter((entry) => {
68
68
  return (
69
69
  entry?.queue?.uuid === selectedQueue?.uuid &&
70
+ (statusFilter.length > 0 ? statusFilter.includes(entry?.status?.uuid) : true) &&
70
71
  (priorityFilter ? entry?.priority?.uuid === priorityFilter : true) &&
71
72
  (serviceAwaitingFilter.length > 0 ? serviceAwaitingFilter.includes(entry?.patient?.uuid) : true) &&
72
73
  (serviceCompletedFilter.length > 0 ? serviceCompletedFilter.includes(entry?.patient?.uuid) : true)
73
74
  );
74
75
  });
75
76
  return filtered;
76
- }, [filters, queueEntries, selectedQueue?.uuid]);
77
+ }, [filters, queueEntries?.length, selectedQueue?.uuid]);
77
78
 
78
79
  const handleTabChange = useCallback(
79
80
  (evt: { selectedIndex: number }) => {
@@ -86,7 +87,9 @@ const QueueTab: React.FC<QueueTabProps> = ({
86
87
  [validQueues, onTabChanged],
87
88
  );
88
89
 
89
- if (isInitialLoad && isLoading) {
90
+ const isLoading = isLoadingQueueEntries && !isValidatingQueueEntries;
91
+
92
+ if (isLoading) {
90
93
  return <TabsSkeleton />;
91
94
  }
92
95
 
@@ -112,18 +115,6 @@ const QueueTab: React.FC<QueueTabProps> = ({
112
115
 
113
116
  return (
114
117
  <div className={styles.queueTab}>
115
- <div className={styles.cards}>
116
- {cards?.map((card) => (
117
- <Card
118
- key={card.title}
119
- title={card.title}
120
- total={card.value}
121
- categories={card.categories}
122
- onClick={card.onClick}
123
- refreshButton={card.refreshButton}
124
- />
125
- ))}
126
- </div>
127
118
  <FiltersHeader filters={filters} onFiltersChanged={onFiltersChanged} />
128
119
  <div className={styles.tabsContainer}>
129
120
  <Tabs selectedIndex={selectedTabIndex} onChange={handleTabChange}>
@@ -135,7 +126,7 @@ const QueueTab: React.FC<QueueTabProps> = ({
135
126
  <TabPanels>
136
127
  {validQueues.map((queue, index) => (
137
128
  <TabPanel key={queue?.uuid}>
138
- {isLoading && !isInitialLoad && (
129
+ {isLoading && (
139
130
  <div className={styles.loadingOverlay}>
140
131
  <InlineLoading description={t('loadingQueueEntries', 'Loading queue entries...')} />
141
132
  </div>
@@ -144,6 +135,10 @@ const QueueTab: React.FC<QueueTabProps> = ({
144
135
  queueEntries={queueEntriesByService}
145
136
  navigatePath={navigatePath}
146
137
  usePatientChart={usePatientChart}
138
+ pagination={pagination}
139
+ onPageSizeChange={(pageSize) => {
140
+ setPageSize(pageSize as number);
141
+ }}
147
142
  />
148
143
  </TabPanel>
149
144
  ))}
@@ -24,4 +24,5 @@
24
24
  display: flex;
25
25
  flex-direction: row;
26
26
  column-gap: layout.$spacing-05;
27
+ margin: layout.$spacing-05;
27
28
  }
@@ -1,4 +1,5 @@
1
1
  import { type OpenmrsResource, type OpenmrsResourceStrict, type Visit } from '@openmrs/esm-framework';
2
+ import { MutableRefObject } from 'react';
2
3
 
3
4
  export interface DashboardConfig {
4
5
  name: string;
@@ -11,8 +12,8 @@ export type Queue = {
11
12
  name: string;
12
13
  location: OpenmrsResource;
13
14
  service: OpenmrsResource;
14
- allowedPriorities: Array<OpenmrsResource>;
15
- allowedStatuses: Array<OpenmrsResource>;
15
+ allowedPriorities?: Array<OpenmrsResource>;
16
+ allowedStatuses?: Array<OpenmrsResource>;
16
17
  queueRooms: Array<OpenmrsResource>;
17
18
  };
18
19
 
@@ -95,3 +96,16 @@ export type QueueFilter = {
95
96
  value: string;
96
97
  label: string;
97
98
  };
99
+
100
+ export type QueueEntriesPagination = {
101
+ totalPages: number;
102
+ totalCount: number;
103
+ currentPage: number;
104
+ currentPageSize: MutableRefObject<number>;
105
+ paginated: boolean;
106
+ showNextButton: boolean;
107
+ showPreviousButton: boolean;
108
+ goTo: (page: number) => void;
109
+ goToNext: () => void;
110
+ goToPrevious: () => void;
111
+ };