Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 51 additions & 0 deletions docs/expandable-data-table-guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -494,6 +494,57 @@ const renderRobustCell = (row: Item, cellId: string) => {
};
```

### Cell Styling Options

The ExpandableDataTable provides several predefined CSS classes for styling individual cells, across a row:

| Class Name | Color Code | Use Case |
|---------------|------------|-----------|
| criticalCell | #da1e28 | For highlighting critical or error values |
| successCell | #198038 | For highlighting successful or positive values |
| warningCell | #f1c21b | For highlighting warning states |
| alertCell | #ff832b | For highlighting items needing attention |

### Usage in renderCell

```tsx
const renderCell = (row: Item, cellId: string) => {
switch (cellId) {
case 'status':
return (
<div className={row.status === 'Critical' ? 'criticalCell' : ''}>
{row.status}
</div>
);
case 'value':
if (row.value > threshold) {
return <span className="alertCell">{row.value}</span>;
}
return row.value;
// Other cases...
}
};
```

You can combine these cell styles with Carbon Design System components:

```tsx
const renderCell = (row: Item, cellId: string) => {
switch (cellId) {
case 'status':
return (
<Tag
type={row.status === 'Critical' ? 'red' : 'green'}
className={row.status === 'Critical' ? 'criticalCell' : ''}
>
{row.status}
</Tag>
);
// Other cases...
}
};
```

### Data Formatting

1. **Format dates consistently**: Use consistent date formatting throughout the application.
Expand Down
221 changes: 221 additions & 0 deletions src/__mocks__/allergyMocks.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,221 @@
import {
FhirAllergyIntolerance,
FhirAllergyIntoleranceBundle,
} from '@types/allergy';

export const mockAllergyIntolerance: FhirAllergyIntolerance = {
resourceType: 'AllergyIntolerance',
id: 'allergy-123',
meta: {
versionId: '1',
lastUpdated: '2023-01-01T12:00:00Z',
},
clinicalStatus: {
coding: [
{
system:
'http://terminology.hl7.org/CodeSystem/allergyintolerance-clinical',
code: 'active',
display: 'Active',
},
],
},
type: 'allergy',
category: ['food'],
criticality: 'high',
code: {
coding: [
{
system: 'http://snomed.info/sct',
code: '91935009',
display: 'Peanut',
},
],
text: 'Peanut Allergy',
},
patient: {
reference: 'Patient/patient-123',
display: 'John Doe',
},
recordedDate: '2023-01-01T12:00:00Z',
recorder: {
reference: 'Practitioner/practitioner-123',
display: 'Dr. Smith',
},
reaction: [
{
manifestation: [
{
coding: [
{
system: 'http://snomed.info/sct',
code: '247472004',
display: 'Hives',
},
],
},
],
severity: 'moderate',
},
],
note: [
{
text: 'Patient experiences severe reaction within minutes of exposure',
},
{
text: 'Requires immediate medical attention if exposed',
},
],
};
export const mockAllergyIntoleranceWithoutNote: FhirAllergyIntolerance = {
...mockAllergyIntolerance,
note: undefined,
};

export const mockAllergyIntoleranceBundle: FhirAllergyIntoleranceBundle = {
resourceType: 'Bundle',
id: 'bundle-123',
meta: {
lastUpdated: '2023-01-01T12:00:00Z',
},
type: 'searchset',
total: 1,
link: [
{
relation: 'self',
url: 'http://example.org/fhir/AllergyIntolerance?patient=patient-123',
},
],
entry: [
{
fullUrl: 'http://example.org/fhir/AllergyIntolerance/allergy-123',
resource: mockAllergyIntolerance,
},
],
};

export const mockEmptyAllergyIntoleranceBundle: FhirAllergyIntoleranceBundle = {
resourceType: 'Bundle',
id: 'bundle-empty',
meta: {
lastUpdated: '2023-01-01T12:00:00Z',
},
type: 'searchset',
total: 0,
link: [
{
relation: 'self',
url: 'http://example.org/fhir/AllergyIntolerance?patient=patient-123',
},
],
};

export const mockAllergyIntoleranceBundleWithoutEntries: FhirAllergyIntoleranceBundle =
{
resourceType: 'Bundle',
id: 'bundle-no-entries',
meta: {
lastUpdated: '2023-01-01T12:00:00Z',
},
type: 'searchset',
total: 0,
link: [
{
relation: 'self',
url: 'http://example.org/fhir/AllergyIntolerance?patient=patient-123',
},
],
};

/**
* Mock allergy with missing fields (recorder, reactions, note)
*/
export const mockAllergyWithMissingFields: FhirAllergyIntolerance = {
resourceType: 'AllergyIntolerance',
id: 'allergy-incomplete',
meta: {
versionId: '1',
lastUpdated: '2023-01-01T12:00:00Z',
},
clinicalStatus: {
coding: [
{
system:
'http://terminology.hl7.org/CodeSystem/allergyintolerance-clinical',
code: 'active',
display: 'Active',
},
],
},
code: {
coding: [
{
system: 'http://snomed.info/sct',
code: '91935009',
display: 'Peanut',
},
],
text: 'Peanut Allergy',
},
patient: {
reference: 'Patient/patient-123',
display: 'John Doe',
},
recordedDate: '2023-01-01T12:00:00Z',
};

export const mockAllergyWithEmptyReactions: FhirAllergyIntolerance = {
...mockAllergyIntolerance,
id: 'allergy-empty-reactions',
reaction: [],
};

export const mockAllergyWithIncompleteReactions: FhirAllergyIntolerance = {
...mockAllergyIntolerance,
id: 'allergy-incomplete-reactions',
reaction: [
{
manifestation: [
{
coding: [],
},
],
},
],
};

export const mockAllergyWithoutClinicalStatusDisplay: FhirAllergyIntolerance = {
...mockAllergyIntolerance,
id: 'allergy-no-status-display',
clinicalStatus: {
coding: [],
},
};

/**
* Mock allergy with multiple detailed notes
*/
export const mockAllergyWithMultipleNotes: FhirAllergyIntolerance = {
...mockAllergyIntolerance,
id: 'allergy-with-notes',
note: [
{
text: 'First documented reaction at age 5',
},
{
text: 'Carries epinephrine auto-injector',
},
{
text: 'Family history of similar allergies',
},
],
};

/**
* Mock allergy with empty notes array
*/
export const mockAllergyWithEmptyNotes: FhirAllergyIntolerance = {
...mockAllergyIntolerance,
id: 'allergy-empty-notes',
note: [],
};
111 changes: 111 additions & 0 deletions src/components/allergies/AllergiesTable.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
import React, { useMemo } from 'react';
import { Tag } from '@carbon/react';
import { ExpandableDataTable } from '@components/expandableDataTable/ExpandableDataTable';
import { usePatientUUID } from '@hooks/usePatientUUID';
import { useAllergies } from '@hooks/useAllergies';
import { formatAllergies } from '@services/allergyService';
import { FormattedAllergy } from '@types/allergy';
import { formatDateTime } from '@utils/date';
import { generateId, capitalize } from '@utils/common';

/**
* Component to display patient allergies in a DataTable with expandable rows
*/
const AllergiesTable: React.FC = () => {
const patientUUID = usePatientUUID();
const { allergies, loading, error } = useAllergies(patientUUID);

// Define table headers
const headers = useMemo(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is the need for useMemo here ? this is a simple constant with no calculation involved

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you defined the headers array directly in the render function (i.e., without useMemo), a new array reference would be created on every render. Even though the contents are the same, React (or child components) may interpret the new reference as a change — especially if the child component (like ExpandableDataTable) is using React.memo() or does deep equality checks to avoid re-renders.

() => [
{ key: 'display', header: 'Allergy' },
{ key: 'manifestation', header: 'Reaction(s)' },
{ key: 'status', header: 'Status' },
{ key: 'severity', header: 'Severity' },
{ key: 'recorder', header: 'Provider' },
{ key: 'recordedDate', header: 'Recorded Date' },
],
[],
);

// Format allergies for display
const formattedAllergies = useMemo(() => {
if (!allergies || allergies.length === 0) return [];
return formatAllergies(allergies);
}, [allergies]);

// Create row class names array for styling rows with severe allergies
const rowClassNames = useMemo(() => {
return formattedAllergies.map((allergy) =>
allergy.reactions?.some((reaction) => reaction.severity === 'severe')
? 'criticalCell'
: '',
);
}, [formattedAllergies]);

// Function to render cell content based on the cell ID
const renderCell = (allergy: FormattedAllergy, cellId: string) => {
switch (cellId) {
case 'display':
return allergy.display;
case 'status':
return (
<Tag type={allergy.status === 'Active' ? 'green' : 'gray'}>
{allergy.status}
</Tag>
);
case 'manifestation':
return allergy.reactions
? allergy.reactions
.map((reaction) => reaction.manifestation.join(', '))
.join(', ')
: 'Not available';
case 'severity':
return allergy.reactions
? allergy.reactions
.map((reaction) => reaction.severity)
.filter((severity): severity is string => !!severity)
.map((severity) => capitalize(severity))
.join(', ')
: 'Not available';
case 'recorder':
return allergy.recorder || 'Not available';
case 'recordedDate':
return formatDateTime(allergy.recordedDate || '');
}
};

// Function to render expanded content for an allergy
const renderExpandedContent = (allergy: FormattedAllergy) => {
if (allergy.note && allergy.note.length > 0) {
return (
<p style={{ padding: '0.5rem' }} key={generateId()}>
{allergy.note.join(', ')}
</p>
);
}
return undefined;
};

return (
<div
style={{ width: '100%', paddingTop: '1rem' }}
data-testid="allergy-table"
>
<ExpandableDataTable
tableTitle="Allergies"
rows={formattedAllergies}
headers={headers}
renderCell={renderCell}
renderExpandedContent={renderExpandedContent}
loading={loading}
error={error}
ariaLabel="Patient allergies"
emptyStateMessage="No allergies found"
rowClassNames={rowClassNames}
/>
</div>
);
};

export default AllergiesTable;
Loading