Skip to content

Commit 7cecb0c

Browse files
authored
BN-47 | Add. Patient Allergies Display Control (#7)
* BN-47 | Add. Row Styling For ExpandableDataTable * BN-47 | Add. Allergies To HomePage * BN-47 | Fix. Default Date Format * BN-47 | Add. Capitalize Common Util * BN-47 | Fix. Remove Expanded View For Content Without Additional Data * Add. Custom Hook For Patient Allergies * Add. Display Control For Patient Allergies * Fix. Failing Test Case * BN-47 | Refactor. Use Import Aliases
1 parent 875a265 commit 7cecb0c

24 files changed

+3040
-77
lines changed

docs/expandable-data-table-guide.md

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -494,6 +494,57 @@ const renderRobustCell = (row: Item, cellId: string) => {
494494
};
495495
```
496496

497+
### Cell Styling Options
498+
499+
The ExpandableDataTable provides several predefined CSS classes for styling individual cells, across a row:
500+
501+
| Class Name | Color Code | Use Case |
502+
|---------------|------------|-----------|
503+
| criticalCell | #da1e28 | For highlighting critical or error values |
504+
| successCell | #198038 | For highlighting successful or positive values |
505+
| warningCell | #f1c21b | For highlighting warning states |
506+
| alertCell | #ff832b | For highlighting items needing attention |
507+
508+
### Usage in renderCell
509+
510+
```tsx
511+
const renderCell = (row: Item, cellId: string) => {
512+
switch (cellId) {
513+
case 'status':
514+
return (
515+
<div className={row.status === 'Critical' ? 'criticalCell' : ''}>
516+
{row.status}
517+
</div>
518+
);
519+
case 'value':
520+
if (row.value > threshold) {
521+
return <span className="alertCell">{row.value}</span>;
522+
}
523+
return row.value;
524+
// Other cases...
525+
}
526+
};
527+
```
528+
529+
You can combine these cell styles with Carbon Design System components:
530+
531+
```tsx
532+
const renderCell = (row: Item, cellId: string) => {
533+
switch (cellId) {
534+
case 'status':
535+
return (
536+
<Tag
537+
type={row.status === 'Critical' ? 'red' : 'green'}
538+
className={row.status === 'Critical' ? 'criticalCell' : ''}
539+
>
540+
{row.status}
541+
</Tag>
542+
);
543+
// Other cases...
544+
}
545+
};
546+
```
547+
497548
### Data Formatting
498549

499550
1. **Format dates consistently**: Use consistent date formatting throughout the application.

src/__mocks__/allergyMocks.ts

Lines changed: 221 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,221 @@
1+
import {
2+
FhirAllergyIntolerance,
3+
FhirAllergyIntoleranceBundle,
4+
} from '@types/allergy';
5+
6+
export const mockAllergyIntolerance: FhirAllergyIntolerance = {
7+
resourceType: 'AllergyIntolerance',
8+
id: 'allergy-123',
9+
meta: {
10+
versionId: '1',
11+
lastUpdated: '2023-01-01T12:00:00Z',
12+
},
13+
clinicalStatus: {
14+
coding: [
15+
{
16+
system:
17+
'http://terminology.hl7.org/CodeSystem/allergyintolerance-clinical',
18+
code: 'active',
19+
display: 'Active',
20+
},
21+
],
22+
},
23+
type: 'allergy',
24+
category: ['food'],
25+
criticality: 'high',
26+
code: {
27+
coding: [
28+
{
29+
system: 'http://snomed.info/sct',
30+
code: '91935009',
31+
display: 'Peanut',
32+
},
33+
],
34+
text: 'Peanut Allergy',
35+
},
36+
patient: {
37+
reference: 'Patient/patient-123',
38+
display: 'John Doe',
39+
},
40+
recordedDate: '2023-01-01T12:00:00Z',
41+
recorder: {
42+
reference: 'Practitioner/practitioner-123',
43+
display: 'Dr. Smith',
44+
},
45+
reaction: [
46+
{
47+
manifestation: [
48+
{
49+
coding: [
50+
{
51+
system: 'http://snomed.info/sct',
52+
code: '247472004',
53+
display: 'Hives',
54+
},
55+
],
56+
},
57+
],
58+
severity: 'moderate',
59+
},
60+
],
61+
note: [
62+
{
63+
text: 'Patient experiences severe reaction within minutes of exposure',
64+
},
65+
{
66+
text: 'Requires immediate medical attention if exposed',
67+
},
68+
],
69+
};
70+
export const mockAllergyIntoleranceWithoutNote: FhirAllergyIntolerance = {
71+
...mockAllergyIntolerance,
72+
note: undefined,
73+
};
74+
75+
export const mockAllergyIntoleranceBundle: FhirAllergyIntoleranceBundle = {
76+
resourceType: 'Bundle',
77+
id: 'bundle-123',
78+
meta: {
79+
lastUpdated: '2023-01-01T12:00:00Z',
80+
},
81+
type: 'searchset',
82+
total: 1,
83+
link: [
84+
{
85+
relation: 'self',
86+
url: 'http://example.org/fhir/AllergyIntolerance?patient=patient-123',
87+
},
88+
],
89+
entry: [
90+
{
91+
fullUrl: 'http://example.org/fhir/AllergyIntolerance/allergy-123',
92+
resource: mockAllergyIntolerance,
93+
},
94+
],
95+
};
96+
97+
export const mockEmptyAllergyIntoleranceBundle: FhirAllergyIntoleranceBundle = {
98+
resourceType: 'Bundle',
99+
id: 'bundle-empty',
100+
meta: {
101+
lastUpdated: '2023-01-01T12:00:00Z',
102+
},
103+
type: 'searchset',
104+
total: 0,
105+
link: [
106+
{
107+
relation: 'self',
108+
url: 'http://example.org/fhir/AllergyIntolerance?patient=patient-123',
109+
},
110+
],
111+
};
112+
113+
export const mockAllergyIntoleranceBundleWithoutEntries: FhirAllergyIntoleranceBundle =
114+
{
115+
resourceType: 'Bundle',
116+
id: 'bundle-no-entries',
117+
meta: {
118+
lastUpdated: '2023-01-01T12:00:00Z',
119+
},
120+
type: 'searchset',
121+
total: 0,
122+
link: [
123+
{
124+
relation: 'self',
125+
url: 'http://example.org/fhir/AllergyIntolerance?patient=patient-123',
126+
},
127+
],
128+
};
129+
130+
/**
131+
* Mock allergy with missing fields (recorder, reactions, note)
132+
*/
133+
export const mockAllergyWithMissingFields: FhirAllergyIntolerance = {
134+
resourceType: 'AllergyIntolerance',
135+
id: 'allergy-incomplete',
136+
meta: {
137+
versionId: '1',
138+
lastUpdated: '2023-01-01T12:00:00Z',
139+
},
140+
clinicalStatus: {
141+
coding: [
142+
{
143+
system:
144+
'http://terminology.hl7.org/CodeSystem/allergyintolerance-clinical',
145+
code: 'active',
146+
display: 'Active',
147+
},
148+
],
149+
},
150+
code: {
151+
coding: [
152+
{
153+
system: 'http://snomed.info/sct',
154+
code: '91935009',
155+
display: 'Peanut',
156+
},
157+
],
158+
text: 'Peanut Allergy',
159+
},
160+
patient: {
161+
reference: 'Patient/patient-123',
162+
display: 'John Doe',
163+
},
164+
recordedDate: '2023-01-01T12:00:00Z',
165+
};
166+
167+
export const mockAllergyWithEmptyReactions: FhirAllergyIntolerance = {
168+
...mockAllergyIntolerance,
169+
id: 'allergy-empty-reactions',
170+
reaction: [],
171+
};
172+
173+
export const mockAllergyWithIncompleteReactions: FhirAllergyIntolerance = {
174+
...mockAllergyIntolerance,
175+
id: 'allergy-incomplete-reactions',
176+
reaction: [
177+
{
178+
manifestation: [
179+
{
180+
coding: [],
181+
},
182+
],
183+
},
184+
],
185+
};
186+
187+
export const mockAllergyWithoutClinicalStatusDisplay: FhirAllergyIntolerance = {
188+
...mockAllergyIntolerance,
189+
id: 'allergy-no-status-display',
190+
clinicalStatus: {
191+
coding: [],
192+
},
193+
};
194+
195+
/**
196+
* Mock allergy with multiple detailed notes
197+
*/
198+
export const mockAllergyWithMultipleNotes: FhirAllergyIntolerance = {
199+
...mockAllergyIntolerance,
200+
id: 'allergy-with-notes',
201+
note: [
202+
{
203+
text: 'First documented reaction at age 5',
204+
},
205+
{
206+
text: 'Carries epinephrine auto-injector',
207+
},
208+
{
209+
text: 'Family history of similar allergies',
210+
},
211+
],
212+
};
213+
214+
/**
215+
* Mock allergy with empty notes array
216+
*/
217+
export const mockAllergyWithEmptyNotes: FhirAllergyIntolerance = {
218+
...mockAllergyIntolerance,
219+
id: 'allergy-empty-notes',
220+
note: [],
221+
};
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
import React, { useMemo } from 'react';
2+
import { Tag } from '@carbon/react';
3+
import { ExpandableDataTable } from '@components/expandableDataTable/ExpandableDataTable';
4+
import { usePatientUUID } from '@hooks/usePatientUUID';
5+
import { useAllergies } from '@hooks/useAllergies';
6+
import { formatAllergies } from '@services/allergyService';
7+
import { FormattedAllergy } from '@types/allergy';
8+
import { formatDateTime } from '@utils/date';
9+
import { generateId, capitalize } from '@utils/common';
10+
11+
/**
12+
* Component to display patient allergies in a DataTable with expandable rows
13+
*/
14+
const AllergiesTable: React.FC = () => {
15+
const patientUUID = usePatientUUID();
16+
const { allergies, loading, error } = useAllergies(patientUUID);
17+
18+
// Define table headers
19+
const headers = useMemo(
20+
() => [
21+
{ key: 'display', header: 'Allergy' },
22+
{ key: 'manifestation', header: 'Reaction(s)' },
23+
{ key: 'status', header: 'Status' },
24+
{ key: 'severity', header: 'Severity' },
25+
{ key: 'recorder', header: 'Provider' },
26+
{ key: 'recordedDate', header: 'Recorded Date' },
27+
],
28+
[],
29+
);
30+
31+
// Format allergies for display
32+
const formattedAllergies = useMemo(() => {
33+
if (!allergies || allergies.length === 0) return [];
34+
return formatAllergies(allergies);
35+
}, [allergies]);
36+
37+
// Create row class names array for styling rows with severe allergies
38+
const rowClassNames = useMemo(() => {
39+
return formattedAllergies.map((allergy) =>
40+
allergy.reactions?.some((reaction) => reaction.severity === 'severe')
41+
? 'criticalCell'
42+
: '',
43+
);
44+
}, [formattedAllergies]);
45+
46+
// Function to render cell content based on the cell ID
47+
const renderCell = (allergy: FormattedAllergy, cellId: string) => {
48+
switch (cellId) {
49+
case 'display':
50+
return allergy.display;
51+
case 'status':
52+
return (
53+
<Tag type={allergy.status === 'Active' ? 'green' : 'gray'}>
54+
{allergy.status}
55+
</Tag>
56+
);
57+
case 'manifestation':
58+
return allergy.reactions
59+
? allergy.reactions
60+
.map((reaction) => reaction.manifestation.join(', '))
61+
.join(', ')
62+
: 'Not available';
63+
case 'severity':
64+
return allergy.reactions
65+
? allergy.reactions
66+
.map((reaction) => reaction.severity)
67+
.filter((severity): severity is string => !!severity)
68+
.map((severity) => capitalize(severity))
69+
.join(', ')
70+
: 'Not available';
71+
case 'recorder':
72+
return allergy.recorder || 'Not available';
73+
case 'recordedDate':
74+
return formatDateTime(allergy.recordedDate || '');
75+
}
76+
};
77+
78+
// Function to render expanded content for an allergy
79+
const renderExpandedContent = (allergy: FormattedAllergy) => {
80+
if (allergy.note && allergy.note.length > 0) {
81+
return (
82+
<p style={{ padding: '0.5rem' }} key={generateId()}>
83+
{allergy.note.join(', ')}
84+
</p>
85+
);
86+
}
87+
return undefined;
88+
};
89+
90+
return (
91+
<div
92+
style={{ width: '100%', paddingTop: '1rem' }}
93+
data-testid="allergy-table"
94+
>
95+
<ExpandableDataTable
96+
tableTitle="Allergies"
97+
rows={formattedAllergies}
98+
headers={headers}
99+
renderCell={renderCell}
100+
renderExpandedContent={renderExpandedContent}
101+
loading={loading}
102+
error={error}
103+
ariaLabel="Patient allergies"
104+
emptyStateMessage="No allergies found"
105+
rowClassNames={rowClassNames}
106+
/>
107+
</div>
108+
);
109+
};
110+
111+
export default AllergiesTable;

0 commit comments

Comments
 (0)