@digihmis/esm-outpatient-app 1.0.4 → 1.0.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/371.js +1 -1
- package/dist/371.js.map +1 -1
- package/dist/409.js +1 -0
- package/dist/409.js.map +1 -0
- package/dist/594.js +1 -1
- package/dist/594.js.map +1 -1
- package/dist/digihmis-esm-outpatient-app.js +2 -2
- package/dist/digihmis-esm-outpatient-app.js.buildmanifest.json +33 -33
- package/dist/main.js +5 -5
- package/dist/main.js.map +1 -1
- package/dist/routes.json +1 -1
- package/package.json +1 -1
- package/src/config-schema.ts +7 -0
- package/src/dashboard.meta.ts +3 -1
- package/src/patient-management/clinical-encounters.component.tsx +16 -9
- package/src/patient-management/patient-management-workspace/patient-management-schema/index.ts +2 -2
- package/src/patient-management/patient-management-workspace/patient-management-workspace.tsx +3 -0
- package/src/patient-management/patient-management.component.tsx +43 -26
- package/src/patient-management/patient-management.scss +33 -5
- package/src/patient-management/results/investigation-results.component.tsx +210 -0
- package/src/patient-management/results/investigation-results.scss +75 -0
- package/src/patient-management/results/results.resource.ts +150 -0
- package/src/queue-room-table/queue-room-table.component.tsx +18 -7
- package/src/queue-room-table/queue-room-table.resource.ts +26 -8
- package/src/queue-room-table/queue-room-table.scss +17 -0
- package/src/queue-room-table/type/index.ts +2 -0
- package/src/routes.json +1 -1
- package/dist/752.js +0 -1
- package/dist/752.js.map +0 -1
package/dist/routes.json
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"$schema":"https://json.openmrs.org/routes.schema.json","backendDependencies":{"fhir2":">=1.2","webservices.rest":">=2.24.0"},"extensions":[{"name":"outpatient-dashboard-header","slot":"outpatient-sidebar-slot","component":"outpatientDashboardHeader","order":0,"online":true,"offline":true},{"name":"outpatient-db-link","slot":"outpatient-dashboard-slot","component":"leftPanelLink","meta":{"name":"outpatient","slot":"outpatient-homepage-dashboard-slot","title":"Outpatient queue"},"order":1,"online":true,"offline":true},{"name":"outpatient-queue-dashboard","slot":"outpatient-homepage-dashboard-slot","component":"outpatientQueueDashboard"},{"name":"care-panel-link","slot":"outpatient-dashboard-slot","component":"carePanelLink","meta":{"name":"outpatient/patient/:patientUuid/file","slot":"outpatient-file-dashboard-slot","title":"Patient file","showOnlyOnRoute":"/
|
|
1
|
+
{"$schema":"https://json.openmrs.org/routes.schema.json","backendDependencies":{"fhir2":">=1.2","webservices.rest":">=2.24.0"},"extensions":[{"name":"outpatient-dashboard-header","slot":"outpatient-sidebar-slot","component":"outpatientDashboardHeader","order":0,"online":true,"offline":true},{"name":"outpatient-db-link","slot":"outpatient-dashboard-slot","component":"leftPanelLink","meta":{"name":"outpatient","slot":"outpatient-homepage-dashboard-slot","title":"Outpatient queue"},"order":1,"online":true,"offline":true},{"name":"outpatient-queue-dashboard","slot":"outpatient-homepage-dashboard-slot","component":"outpatientQueueDashboard"},{"name":"care-panel-link","slot":"outpatient-dashboard-slot","component":"carePanelLink","meta":{"name":"outpatient/patient/:patientUuid/file","slot":"outpatient-file-dashboard-slot","title":"Patient file","showOnlyOnRoute":"/patient/"},"order":2,"online":true,"offline":true},{"name":"care-panel-history","slot":"outpatient-file-dashboard-slot","component":"carePanel"}],"workspaces":[],"version":"1.0.5"}
|
package/package.json
CHANGED
package/src/config-schema.ts
CHANGED
|
@@ -210,6 +210,12 @@ export const configSchema: ConfigSchema = {
|
|
|
210
210
|
_description: 'Procedure Concept SET UUID',
|
|
211
211
|
_default: '165418AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA',
|
|
212
212
|
},
|
|
213
|
+
radiologyConceptSetUuid: {
|
|
214
|
+
_type: Type.String,
|
|
215
|
+
_description:
|
|
216
|
+
'Radiology/Imaging Concept SET UUID. Used on the patient file to split a patient’s studies into the Imaging tab (concepts in this set) vs the Procedures tab (everything else).',
|
|
217
|
+
_default: '164068AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA',
|
|
218
|
+
},
|
|
213
219
|
visitAttributeTypes: {
|
|
214
220
|
paymentMethods: {
|
|
215
221
|
_type: Type.String,
|
|
@@ -332,6 +338,7 @@ export type ConfigObject = {
|
|
|
332
338
|
};
|
|
333
339
|
procedureOrderTypeUuid: string;
|
|
334
340
|
procedureConceptSetUuid: string;
|
|
341
|
+
radiologyConceptSetUuid: string;
|
|
335
342
|
visitAttributeTypes: {
|
|
336
343
|
paymentMethods: string;
|
|
337
344
|
insuranceScheme: string;
|
package/src/dashboard.meta.ts
CHANGED
|
@@ -12,5 +12,7 @@ export const carePanelMeta = {
|
|
|
12
12
|
slot: 'outpatient-file-dashboard-slot',
|
|
13
13
|
title: 'Patient file',
|
|
14
14
|
icon: UserIdentification,
|
|
15
|
-
|
|
15
|
+
// Show the link throughout a patient's context (any /patient/<uuid>/... route), not only on the
|
|
16
|
+
// /file route itself — otherwise it vanishes the moment you're on any other patient sub-route.
|
|
17
|
+
showOnlyOnRoute: '/patient/',
|
|
16
18
|
};
|
|
@@ -28,6 +28,7 @@ import {
|
|
|
28
28
|
useClinicalEncounterFieldLabels,
|
|
29
29
|
} from './clinical-encounters.resource';
|
|
30
30
|
import styles from './patient-management.scss';
|
|
31
|
+
import { CardHeader } from '@openmrs/esm-patient-common-lib';
|
|
31
32
|
|
|
32
33
|
interface ClinicalEncountersListProps {
|
|
33
34
|
patientUuid: string;
|
|
@@ -62,7 +63,6 @@ function providerName(encounter: ClinicalEncounter): string | null {
|
|
|
62
63
|
return encounter.encounterProviders?.[0]?.provider?.display ?? null;
|
|
63
64
|
}
|
|
64
65
|
|
|
65
|
-
/** The saved clinical-encounter records for a patient, shown on the Clinical encounter tab. */
|
|
66
66
|
const ClinicalEncountersList: React.FC<ClinicalEncountersListProps> = ({ patientUuid, onStartConsultation }) => {
|
|
67
67
|
const { t } = useTranslation();
|
|
68
68
|
const { encounters, isLoading } = useClinicalEncounters(patientUuid);
|
|
@@ -117,17 +117,24 @@ const ClinicalEncountersList: React.FC<ClinicalEncountersListProps> = ({ patient
|
|
|
117
117
|
|
|
118
118
|
return (
|
|
119
119
|
<div className={styles.encounterList}>
|
|
120
|
-
<div className={styles.
|
|
121
|
-
<
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
</Button>
|
|
120
|
+
<div className={styles.cardEncounter}>
|
|
121
|
+
<CardHeader title={t('clinicalEncounters', 'Clinical encounters ')}>
|
|
122
|
+
<Button kind="ghost" size="sm" renderIcon={Add} onClick={onStartConsultation}>
|
|
123
|
+
{t('startConsultation', 'Start consultation')}
|
|
124
|
+
</Button>
|
|
125
|
+
</CardHeader>
|
|
127
126
|
</div>
|
|
128
127
|
|
|
129
128
|
<DataTable rows={rows} headers={headers} size="sm" useZebraStyles>
|
|
130
|
-
{({
|
|
129
|
+
{({
|
|
130
|
+
rows,
|
|
131
|
+
headers,
|
|
132
|
+
getHeaderProps,
|
|
133
|
+
getRowProps,
|
|
134
|
+
getExpandedRowProps,
|
|
135
|
+
getTableProps,
|
|
136
|
+
getTableContainerProps,
|
|
137
|
+
}) => (
|
|
131
138
|
<TableContainer {...getTableContainerProps()}>
|
|
132
139
|
<Table {...getTableProps()} aria-label={t('clinicalEncounters', 'Clinical encounters')}>
|
|
133
140
|
<TableHead>
|
package/src/patient-management/patient-management-workspace/patient-management-schema/index.ts
CHANGED
|
@@ -83,8 +83,8 @@ export const BaseClinicalEncounterSchema = z.object({
|
|
|
83
83
|
// Lymphadenopathy (when Lymphadenopathy is selected)
|
|
84
84
|
siteLymphadenopathy: z.array(genericItemSchema).optional().default([]),
|
|
85
85
|
|
|
86
|
-
// Section 3: Diagnosis
|
|
87
|
-
primaryDiagnosis: z.array(genericItemSchema).
|
|
86
|
+
// Section 3: Diagnosis — a primary diagnosis is mandatory before an encounter can be saved.
|
|
87
|
+
primaryDiagnosis: z.array(genericItemSchema).min(1, 'Please select at least one primary diagnosis'),
|
|
88
88
|
secondaryDiagnosis: z.array(genericItemSchema).optional().default([]),
|
|
89
89
|
diagnosisNotes: z.string().optional(),
|
|
90
90
|
|
package/src/patient-management/patient-management-workspace/patient-management-workspace.tsx
CHANGED
|
@@ -536,6 +536,9 @@ const ClinicalEncounterWorkspace: React.FC<ClinicalEncounterWorkspaceProps> = ({
|
|
|
536
536
|
if (hasLymphadenopathy) {
|
|
537
537
|
fieldsToValidate.push('siteLymphadenopathy');
|
|
538
538
|
}
|
|
539
|
+
} else if (currentSection === 3) {
|
|
540
|
+
// Diagnosis step: a primary diagnosis is mandatory before leaving this step.
|
|
541
|
+
fieldsToValidate.push('primaryDiagnosis');
|
|
539
542
|
}
|
|
540
543
|
|
|
541
544
|
const isValid = fieldsToValidate.length > 0 ? await trigger(fieldsToValidate) : true;
|
|
@@ -12,7 +12,15 @@ import {
|
|
|
12
12
|
Tag,
|
|
13
13
|
Tile,
|
|
14
14
|
} from '@carbon/react';
|
|
15
|
-
import {
|
|
15
|
+
import {
|
|
16
|
+
Activity,
|
|
17
|
+
Calendar,
|
|
18
|
+
ChartLineData,
|
|
19
|
+
Medication,
|
|
20
|
+
Microscope,
|
|
21
|
+
Stethoscope,
|
|
22
|
+
type CarbonIconType,
|
|
23
|
+
} from '@carbon/react/icons';
|
|
16
24
|
import {
|
|
17
25
|
ExtensionSlot,
|
|
18
26
|
formatDatetime,
|
|
@@ -23,6 +31,7 @@ import {
|
|
|
23
31
|
} from '@openmrs/esm-framework';
|
|
24
32
|
import PatientBanner from './patient-banner/patient-banner.component';
|
|
25
33
|
import ClinicalEncountersList from './clinical-encounters.component';
|
|
34
|
+
import { LabResults, ImagingResults, ProcedureResults } from './results/investigation-results.component';
|
|
26
35
|
import { getPatientUuidFromUrl } from './utils';
|
|
27
36
|
import styles from './patient-management.scss';
|
|
28
37
|
|
|
@@ -82,6 +91,14 @@ const CarePanel: React.FC = () => {
|
|
|
82
91
|
emptyTitle: t('noImagingTitle', 'No imaging results'),
|
|
83
92
|
emptyBody: t('noImagingBody', 'Radiology and imaging results will appear here.'),
|
|
84
93
|
},
|
|
94
|
+
{
|
|
95
|
+
key: 'procedures',
|
|
96
|
+
label: t('procedures', 'Procedures'),
|
|
97
|
+
icon: Activity,
|
|
98
|
+
slot: 'outpatient-procedures-slot',
|
|
99
|
+
emptyTitle: t('noProcedureTitle', 'No procedure reports'),
|
|
100
|
+
emptyBody: t('noProcedureBody', 'Procedure reports for this patient will appear here.'),
|
|
101
|
+
},
|
|
85
102
|
{
|
|
86
103
|
key: 'prescriptions',
|
|
87
104
|
label: t('prescriptions', 'Prescriptions'),
|
|
@@ -102,33 +119,33 @@ const CarePanel: React.FC = () => {
|
|
|
102
119
|
|
|
103
120
|
return (
|
|
104
121
|
<div className={styles.container}>
|
|
105
|
-
|
|
106
|
-
<
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
patient && <PatientBanner patientUuid={patientUuid} patient={patient} hideActionsOverflow />
|
|
111
|
-
)}
|
|
112
|
-
|
|
113
|
-
</Tile>
|
|
114
|
-
</Layer>
|
|
122
|
+
{isLoading ? (
|
|
123
|
+
<SkeletonText heading width="50%" />
|
|
124
|
+
) : (
|
|
125
|
+
patient && <PatientBanner patientUuid={patientUuid} patient={patient} hideActionsOverflow />
|
|
126
|
+
)}
|
|
115
127
|
|
|
116
128
|
<ExtensionSlot name="patient-vitals-header-slot" state={{ patientUuid }} className={styles.vitals} />
|
|
117
|
-
|
|
118
|
-
<
|
|
119
|
-
<
|
|
120
|
-
|
|
121
|
-
{
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
{tabs.map((tab) => (
|
|
129
|
+
<div className={styles.tabsContainer}>
|
|
130
|
+
<Tabs>
|
|
131
|
+
<TabList aria-label={t('carePanel', 'Care panel')} scrollDebounceWait={200}>
|
|
132
|
+
{tabs.map((tab) => (
|
|
133
|
+
<Tab key={tab.key} renderIcon={tab.icon}>
|
|
134
|
+
{tab.label}
|
|
135
|
+
</Tab>
|
|
136
|
+
))}
|
|
137
|
+
</TabList>
|
|
138
|
+
<TabPanels>
|
|
139
|
+
{tabs.map((tab) => (
|
|
129
140
|
<TabPanel key={tab.key} className={styles.tabPanel}>
|
|
130
141
|
{tab.key === 'encounter' ? (
|
|
131
142
|
<ClinicalEncountersList patientUuid={patientUuid} onStartConsultation={startConsultation} />
|
|
143
|
+
) : tab.key === 'labs' ? (
|
|
144
|
+
<LabResults patientUuid={patientUuid} />
|
|
145
|
+
) : tab.key === 'imaging' ? (
|
|
146
|
+
<ImagingResults patientUuid={patientUuid} />
|
|
147
|
+
) : tab.key === 'procedures' ? (
|
|
148
|
+
<ProcedureResults patientUuid={patientUuid} />
|
|
132
149
|
) : (
|
|
133
150
|
<>
|
|
134
151
|
<ExtensionSlot name={tab.slot} state={{ patientUuid, visitUuid: activeVisit?.uuid }} />
|
|
@@ -137,9 +154,9 @@ const CarePanel: React.FC = () => {
|
|
|
137
154
|
)}
|
|
138
155
|
</TabPanel>
|
|
139
156
|
))}
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
</
|
|
157
|
+
</TabPanels>
|
|
158
|
+
</Tabs>
|
|
159
|
+
</div>
|
|
143
160
|
</div>
|
|
144
161
|
);
|
|
145
162
|
};
|
|
@@ -2,16 +2,19 @@
|
|
|
2
2
|
@use '@carbon/type';
|
|
3
3
|
|
|
4
4
|
.container {
|
|
5
|
-
|
|
6
|
-
|
|
5
|
+
margin: 1rem auto;
|
|
6
|
+
|
|
7
7
|
}
|
|
8
8
|
|
|
9
|
-
/* ---- Header card: banner + visit status + primary action ---- */
|
|
10
9
|
.headerCard {
|
|
11
10
|
padding: layout.$spacing-05;
|
|
12
11
|
margin-bottom: layout.$spacing-05;
|
|
13
12
|
}
|
|
14
13
|
|
|
14
|
+
.cardEncounter {
|
|
15
|
+
padding-top: layout.$spacing-03;
|
|
16
|
+
}
|
|
17
|
+
|
|
15
18
|
.actionBar {
|
|
16
19
|
display: flex;
|
|
17
20
|
align-items: center;
|
|
@@ -29,10 +32,8 @@
|
|
|
29
32
|
|
|
30
33
|
.vitals {
|
|
31
34
|
display: block;
|
|
32
|
-
margin-bottom: layout.$spacing-05;
|
|
33
35
|
}
|
|
34
36
|
|
|
35
|
-
/* ---- Tabs card ---- */
|
|
36
37
|
.tabsCard {
|
|
37
38
|
border: 1px solid var(--cds-border-subtle);
|
|
38
39
|
}
|
|
@@ -189,3 +190,30 @@
|
|
|
189
190
|
text-align: right;
|
|
190
191
|
width: 3rem;
|
|
191
192
|
}
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
.tabsContainer {
|
|
196
|
+
padding-left: layout.$spacing-06;
|
|
197
|
+
padding-right: layout.$spacing-06;
|
|
198
|
+
background: var(--cds-field);
|
|
199
|
+
|
|
200
|
+
:global(.omrs-breakpoint-lt-tablet) & {
|
|
201
|
+
padding-left: layout.$spacing-03;
|
|
202
|
+
padding-right: layout.$spacing-03;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
.tabsHeaderRow {
|
|
207
|
+
display: flex;
|
|
208
|
+
align-items: center;
|
|
209
|
+
justify-content: space-between;
|
|
210
|
+
margin-right: 0.51rem;
|
|
211
|
+
|
|
212
|
+
:global(.omrs-breakpoint-lt-desktop) & {
|
|
213
|
+
gap: layout.$spacing-03;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
:global(.omrs-breakpoint-lt-tablet) & {
|
|
217
|
+
gap: layout.$spacing-03;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { useTranslation } from 'react-i18next';
|
|
3
|
+
import {
|
|
4
|
+
InlineLoading,
|
|
5
|
+
Layer,
|
|
6
|
+
Table,
|
|
7
|
+
TableBody,
|
|
8
|
+
TableCell,
|
|
9
|
+
TableHead,
|
|
10
|
+
TableHeader,
|
|
11
|
+
TableRow,
|
|
12
|
+
Tag,
|
|
13
|
+
Tile,
|
|
14
|
+
} from '@carbon/react';
|
|
15
|
+
import { formatDatetime, parseDate, useConfig } from '@openmrs/esm-framework';
|
|
16
|
+
import { type ConfigObject } from '../../config-schema';
|
|
17
|
+
import {
|
|
18
|
+
formatLabValue,
|
|
19
|
+
useConceptSetMembers,
|
|
20
|
+
useLabResults,
|
|
21
|
+
useProcedureRecords,
|
|
22
|
+
type ProcedureRecord,
|
|
23
|
+
} from './results.resource';
|
|
24
|
+
import styles from './investigation-results.scss';
|
|
25
|
+
|
|
26
|
+
/** Map a report/procedure/order status to a Carbon tag colour. */
|
|
27
|
+
function statusColor(status?: string): string {
|
|
28
|
+
const s = (status ?? '').toLowerCase();
|
|
29
|
+
if (s.includes('approv') || s.includes('complet') || s.includes('verif')) {
|
|
30
|
+
return 'green';
|
|
31
|
+
}
|
|
32
|
+
if (s.includes('prelim') || s.includes('report')) {
|
|
33
|
+
return 'blue';
|
|
34
|
+
}
|
|
35
|
+
if (s.includes('progress') || s.includes('receiv')) {
|
|
36
|
+
return 'teal';
|
|
37
|
+
}
|
|
38
|
+
return 'gray';
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function formatDate(value?: string): string {
|
|
42
|
+
if (!value) {
|
|
43
|
+
return '';
|
|
44
|
+
}
|
|
45
|
+
try {
|
|
46
|
+
return formatDatetime(parseDate(value), { mode: 'standard' });
|
|
47
|
+
} catch {
|
|
48
|
+
return value;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const Loading: React.FC = () => {
|
|
53
|
+
const { t } = useTranslation();
|
|
54
|
+
return (
|
|
55
|
+
<div className={styles.loading}>
|
|
56
|
+
<InlineLoading description={t('loadingResults', 'Loading results…')} />
|
|
57
|
+
</div>
|
|
58
|
+
);
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
const Empty: React.FC<{ message: string }> = ({ message }) => (
|
|
62
|
+
<Layer>
|
|
63
|
+
<Tile>
|
|
64
|
+
<p className={styles.empty}>{message}</p>
|
|
65
|
+
</Tile>
|
|
66
|
+
</Layer>
|
|
67
|
+
);
|
|
68
|
+
|
|
69
|
+
// ---- Lab results --------------------------------------------------------------
|
|
70
|
+
|
|
71
|
+
export const LabResults: React.FC<{ patientUuid: string }> = ({ patientUuid }) => {
|
|
72
|
+
const { t } = useTranslation();
|
|
73
|
+
const { labResults, isLoading, error } = useLabResults(patientUuid);
|
|
74
|
+
|
|
75
|
+
if (isLoading) {
|
|
76
|
+
return <Loading />;
|
|
77
|
+
}
|
|
78
|
+
if (error) {
|
|
79
|
+
return <p className={styles.error}>{t('labResultsError', 'Could not load lab results.')}</p>;
|
|
80
|
+
}
|
|
81
|
+
if (!labResults.length) {
|
|
82
|
+
return <Empty message={t('noLabResultsBody', 'Laboratory results for this patient will appear here.')} />;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return (
|
|
86
|
+
<div className={styles.list}>
|
|
87
|
+
{labResults.map((record) => (
|
|
88
|
+
<Layer key={record.orderUuid}>
|
|
89
|
+
<Tile className={styles.card}>
|
|
90
|
+
<div className={styles.cardHeader}>
|
|
91
|
+
<h4 className={styles.title}>{record.testName}</h4>
|
|
92
|
+
<span className={styles.date}>{formatDate(record.date)}</span>
|
|
93
|
+
</div>
|
|
94
|
+
{record.results.length ? (
|
|
95
|
+
<Table size="sm" className={styles.valuesTable}>
|
|
96
|
+
<TableHead>
|
|
97
|
+
<TableRow>
|
|
98
|
+
<TableHeader>{t('test', 'Test')}</TableHeader>
|
|
99
|
+
<TableHeader>{t('result', 'Result')}</TableHeader>
|
|
100
|
+
</TableRow>
|
|
101
|
+
</TableHead>
|
|
102
|
+
<TableBody>
|
|
103
|
+
{record.results.map((value, index) => (
|
|
104
|
+
<TableRow key={`${record.orderUuid}-${index}`}>
|
|
105
|
+
<TableCell>{value.concept?.display ?? '—'}</TableCell>
|
|
106
|
+
<TableCell>{formatLabValue(value.value)}</TableCell>
|
|
107
|
+
</TableRow>
|
|
108
|
+
))}
|
|
109
|
+
</TableBody>
|
|
110
|
+
</Table>
|
|
111
|
+
) : (
|
|
112
|
+
<p className={styles.empty}>{t('noValues', 'No recorded values.')}</p>
|
|
113
|
+
)}
|
|
114
|
+
</Tile>
|
|
115
|
+
</Layer>
|
|
116
|
+
))}
|
|
117
|
+
</div>
|
|
118
|
+
);
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
// ---- Shared report list (imaging + procedure) ---------------------------------
|
|
122
|
+
|
|
123
|
+
const ReportCard: React.FC<{ record: ProcedureRecord }> = ({ record }) => {
|
|
124
|
+
const { t } = useTranslation();
|
|
125
|
+
const findings = record.procedureReport || record.preliminaryReport;
|
|
126
|
+
return (
|
|
127
|
+
<Layer>
|
|
128
|
+
<Tile className={styles.card}>
|
|
129
|
+
<div className={styles.cardHeader}>
|
|
130
|
+
<h4 className={styles.title}>{record.concept?.display ?? t('study', 'Study')}</h4>
|
|
131
|
+
<div className={styles.cardHeader}>
|
|
132
|
+
{record.status && <Tag type={statusColor(record.status)}>{record.status}</Tag>}
|
|
133
|
+
<span className={styles.date}>{formatDate(record.startDatetime)}</span>
|
|
134
|
+
</div>
|
|
135
|
+
</div>
|
|
136
|
+
{findings ? (
|
|
137
|
+
<div className={styles.section}>
|
|
138
|
+
<p className={styles.sectionLabel}>{t('findings', 'Findings')}</p>
|
|
139
|
+
<p className={styles.reportText}>{findings}</p>
|
|
140
|
+
</div>
|
|
141
|
+
) : (
|
|
142
|
+
<p className={styles.empty}>{t('noReportYet', 'No report entered yet.')}</p>
|
|
143
|
+
)}
|
|
144
|
+
{record.impressions && (
|
|
145
|
+
<div className={styles.section}>
|
|
146
|
+
<p className={styles.sectionLabel}>{t('impression', 'Impression')}</p>
|
|
147
|
+
<p className={styles.reportText}>{record.impressions}</p>
|
|
148
|
+
</div>
|
|
149
|
+
)}
|
|
150
|
+
</Tile>
|
|
151
|
+
</Layer>
|
|
152
|
+
);
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
const ProcedureReportList: React.FC<{
|
|
156
|
+
patientUuid: string;
|
|
157
|
+
variant: 'imaging' | 'procedure';
|
|
158
|
+
emptyMessage: string;
|
|
159
|
+
}> = ({ patientUuid, variant, emptyMessage }) => {
|
|
160
|
+
const { t } = useTranslation();
|
|
161
|
+
const { radiologyConceptSetUuid } = useConfig<ConfigObject>();
|
|
162
|
+
const { procedures, isLoading, error } = useProcedureRecords(patientUuid);
|
|
163
|
+
const radiologyMembers = useConceptSetMembers(radiologyConceptSetUuid);
|
|
164
|
+
|
|
165
|
+
if (isLoading) {
|
|
166
|
+
return <Loading />;
|
|
167
|
+
}
|
|
168
|
+
if (error) {
|
|
169
|
+
return <p className={styles.error}>{t('studiesError', 'Could not load studies.')}</p>;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const filtered = procedures.filter((record) => {
|
|
173
|
+
const isImaging = record.concept ? radiologyMembers.has(record.concept.uuid) : false;
|
|
174
|
+
return variant === 'imaging' ? isImaging : !isImaging;
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
if (!filtered.length) {
|
|
178
|
+
return <Empty message={emptyMessage} />;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
return (
|
|
182
|
+
<div className={styles.list}>
|
|
183
|
+
{filtered.map((record) => (
|
|
184
|
+
<ReportCard key={record.uuid} record={record} />
|
|
185
|
+
))}
|
|
186
|
+
</div>
|
|
187
|
+
);
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
export const ImagingResults: React.FC<{ patientUuid: string }> = ({ patientUuid }) => {
|
|
191
|
+
const { t } = useTranslation();
|
|
192
|
+
return (
|
|
193
|
+
<ProcedureReportList
|
|
194
|
+
patientUuid={patientUuid}
|
|
195
|
+
variant="imaging"
|
|
196
|
+
emptyMessage={t('noImagingBody', 'Radiology and imaging results will appear here.')}
|
|
197
|
+
/>
|
|
198
|
+
);
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
export const ProcedureResults: React.FC<{ patientUuid: string }> = ({ patientUuid }) => {
|
|
202
|
+
const { t } = useTranslation();
|
|
203
|
+
return (
|
|
204
|
+
<ProcedureReportList
|
|
205
|
+
patientUuid={patientUuid}
|
|
206
|
+
variant="procedure"
|
|
207
|
+
emptyMessage={t('noProcedureBody', 'Procedure reports for this patient will appear here.')}
|
|
208
|
+
/>
|
|
209
|
+
);
|
|
210
|
+
};
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
@use '@carbon/layout';
|
|
2
|
+
@use '@carbon/type';
|
|
3
|
+
@use '@carbon/colors';
|
|
4
|
+
|
|
5
|
+
.list {
|
|
6
|
+
display: flex;
|
|
7
|
+
flex-direction: column;
|
|
8
|
+
gap: layout.$spacing-04;
|
|
9
|
+
padding: layout.$spacing-03 0;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
.card {
|
|
13
|
+
padding: layout.$spacing-05;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
.cardHeader {
|
|
17
|
+
display: flex;
|
|
18
|
+
align-items: center;
|
|
19
|
+
justify-content: space-between;
|
|
20
|
+
gap: layout.$spacing-03;
|
|
21
|
+
flex-wrap: wrap;
|
|
22
|
+
margin-bottom: layout.$spacing-03;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
.title {
|
|
26
|
+
@include type.type-style('heading-compact-02');
|
|
27
|
+
margin: 0;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
.date {
|
|
31
|
+
@include type.type-style('label-01');
|
|
32
|
+
color: colors.$gray-60;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
.section {
|
|
36
|
+
margin-top: layout.$spacing-04;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
.sectionLabel {
|
|
40
|
+
@include type.type-style('label-01');
|
|
41
|
+
color: colors.$gray-60;
|
|
42
|
+
text-transform: uppercase;
|
|
43
|
+
letter-spacing: 0.04em;
|
|
44
|
+
margin-bottom: layout.$spacing-02;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
.reportText {
|
|
48
|
+
@include type.type-style('body-01');
|
|
49
|
+
white-space: pre-wrap;
|
|
50
|
+
margin: 0;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
.valuesTable {
|
|
54
|
+
width: 100%;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
.loading {
|
|
58
|
+
display: flex;
|
|
59
|
+
justify-content: center;
|
|
60
|
+
padding: layout.$spacing-06;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
.empty {
|
|
64
|
+
@include type.type-style('body-01');
|
|
65
|
+
color: colors.$gray-60;
|
|
66
|
+
padding: layout.$spacing-05;
|
|
67
|
+
text-align: center;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
.error {
|
|
71
|
+
@include type.type-style('body-01');
|
|
72
|
+
color: colors.$red-60;
|
|
73
|
+
padding: layout.$spacing-05;
|
|
74
|
+
text-align: center;
|
|
75
|
+
}
|