-
Notifications
You must be signed in to change notification settings - Fork 242
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
(feat) O3-4083 : Standardize lab result ranges: display absolute min/max values to match validation rules #2161
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
@@ -15,13 +15,34 @@ interface ResultFormFieldProps { | |||||||||||||||||||||||||||||||||||||
const ResultFormField: React.FC<ResultFormFieldProps> = ({ concept, control, defaultValue, errors }) => { | ||||||||||||||||||||||||||||||||||||||
const { t } = useTranslation(); | ||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||
const printValueRange = (concept: LabOrderConcept) => { | ||||||||||||||||||||||||||||||||||||||
if (concept?.datatype?.display === 'Numeric') { | ||||||||||||||||||||||||||||||||||||||
const maxVal = Math.max(concept?.hiAbsolute, concept?.hiCritical, concept?.hiNormal); | ||||||||||||||||||||||||||||||||||||||
const minVal = Math.min(concept?.lowAbsolute, concept?.lowCritical, concept?.lowNormal); | ||||||||||||||||||||||||||||||||||||||
return ` (${minVal ?? 0} - ${maxVal > 0 ? maxVal : '--'} ${concept?.units ?? ''})`; | ||||||||||||||||||||||||||||||||||||||
// TODO: Reference ranges should be dynamically adjusted based on patient demographics: | ||||||||||||||||||||||||||||||||||||||
// - Age-specific ranges (e.g., pediatric vs adult values) | ||||||||||||||||||||||||||||||||||||||
// - Gender-specific ranges where applicable | ||||||||||||||||||||||||||||||||||||||
const formatLabRange = (concept: LabOrderConcept) => { | ||||||||||||||||||||||||||||||||||||||
const { | ||||||||||||||||||||||||||||||||||||||
datatype: { hl7Abbreviation }, | ||||||||||||||||||||||||||||||||||||||
} = concept ?? {}; | ||||||||||||||||||||||||||||||||||||||
if (hl7Abbreviation !== 'NM') return ''; | ||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||
const { hiAbsolute, lowAbsolute, units } = concept; | ||||||||||||||||||||||||||||||||||||||
const displayUnit = units ? ` ${units}` : ''; | ||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||
const hasLowerLimit = lowAbsolute !== null && lowAbsolute !== undefined; | ||||||||||||||||||||||||||||||||||||||
const hasUpperLimit = hiAbsolute !== null && hiAbsolute !== undefined; | ||||||||||||||||||||||||||||||||||||||
Comment on lines
+30
to
+31
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||
if (hasLowerLimit && hasUpperLimit) { | ||||||||||||||||||||||||||||||||||||||
return ` (${lowAbsolute} - ${hiAbsolute}) ${displayUnit}`; | ||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||
if (hasUpperLimit) { | ||||||||||||||||||||||||||||||||||||||
return ` (<= ${hiAbsolute}) ${displayUnit}`; | ||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||
if (hasLowerLimit) { | ||||||||||||||||||||||||||||||||||||||
return ` (>= ${lowAbsolute}) ${displayUnit}`; | ||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||
Comment on lines
+33
to
43
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. While not functionally different, this makes the logic a little clearer:
Suggested change
|
||||||||||||||||||||||||||||||||||||||
return ''; | ||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||
return ` (${displayUnit})`; | ||||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
So we don't return a string with parentheses and spaces when there are no ranges and no units. |
||||||||||||||||||||||||||||||||||||||
}; | ||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||
const getSavedMemberValue = (conceptUuid: string, dataType: string) => { | ||||||||||||||||||||||||||||||||||||||
|
@@ -63,7 +84,7 @@ const ResultFormField: React.FC<ResultFormFieldProps> = ({ concept, control, def | |||||||||||||||||||||||||||||||||||||
hideSteppers | ||||||||||||||||||||||||||||||||||||||
id={concept.uuid} | ||||||||||||||||||||||||||||||||||||||
key={concept.uuid} | ||||||||||||||||||||||||||||||||||||||
label={concept?.display + printValueRange(concept)} | ||||||||||||||||||||||||||||||||||||||
label={concept?.display + formatLabRange(concept)} | ||||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Not an issue created by this PR, but I don't think this works the way we'd want if
Suggested change
|
||||||||||||||||||||||||||||||||||||||
onChange={(event) => field.onChange(parseFloat(event.target.value))} | ||||||||||||||||||||||||||||||||||||||
value={field.value || ''} | ||||||||||||||||||||||||||||||||||||||
invalidText={errors[concept.uuid]?.message} | ||||||||||||||||||||||||||||||||||||||
|
@@ -133,7 +154,7 @@ const ResultFormField: React.FC<ResultFormFieldProps> = ({ concept, control, def | |||||||||||||||||||||||||||||||||||||
hideSteppers | ||||||||||||||||||||||||||||||||||||||
id={`number-${member.uuid}`} | ||||||||||||||||||||||||||||||||||||||
key={member.uuid} | ||||||||||||||||||||||||||||||||||||||
label={member?.display + printValueRange(member)} | ||||||||||||||||||||||||||||||||||||||
label={member?.display + formatLabRange(member)} | ||||||||||||||||||||||||||||||||||||||
onChange={(event) => field.onChange(parseFloat(event.target.value))} | ||||||||||||||||||||||||||||||||||||||
value={field.value || ''} | ||||||||||||||||||||||||||||||||||||||
invalidText={errors[member.uuid]?.message} | ||||||||||||||||||||||||||||||||||||||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -99,7 +99,7 @@ describe('LabResultsForm', () => { | |
const user = userEvent.setup(); | ||
render(<LabResultsForm {...testProps} />); | ||
|
||
const input = await screen.findByLabelText(`Test Concept (0 - 100 mg/dL)`); | ||
const input = await screen.findByLabelText(`Test Concept (0 - 100) mg/dL`); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. See I actually think the original text here is better than this version, since the numbers should be grouped with the units. |
||
await user.type(input, '150'); | ||
|
||
const saveButton = screen.getByRole('button', { name: /Save and close/i }); | ||
|
@@ -135,7 +135,9 @@ describe('LabResultsForm', () => { | |
}); | ||
render(<LabResultsForm {...testProps} />); | ||
|
||
const input = await screen.findByLabelText(`Test Concept (0 - 100 mg/dL)`); | ||
const input = screen.getByRole('spinbutton', { | ||
name: /Test Concept/i, | ||
}); | ||
await user.type(input, '50.5'); | ||
|
||
const saveButton = screen.getByRole('button', { name: /Save and close/i }); | ||
|
@@ -150,7 +152,7 @@ describe('LabResultsForm', () => { | |
const user = userEvent.setup(); | ||
render(<LabResultsForm {...testProps} />); | ||
|
||
const input = await screen.findByLabelText(`Test Concept (0 - 100 mg/dL)`); | ||
const input = await screen.findByLabelText(`Test Concept (0 - 100) mg/dL`); | ||
await user.type(input, '50.5'); | ||
|
||
const saveButton = screen.getByRole('button', { name: /Save and close/i }); | ||
|
@@ -166,7 +168,7 @@ describe('LabResultsForm', () => { | |
const user = userEvent.setup(); | ||
render(<LabResultsForm {...testProps} />); | ||
|
||
const input = await screen.findByLabelText(`Test Concept (0 - 100 mg/dL)`); | ||
const input = await screen.findByLabelText(`Test Concept (0 - 100) mg/dL`); | ||
await user.type(input, '-50'); | ||
|
||
const saveButton = screen.getByRole('button', { name: /Save and close/i }); | ||
|
@@ -181,7 +183,7 @@ describe('LabResultsForm', () => { | |
const user = userEvent.setup(); | ||
render(<LabResultsForm {...testProps} />); | ||
|
||
const input = await screen.findByLabelText(`Test Concept (0 - 100 mg/dL)`); | ||
const input = await screen.findByLabelText('Test Concept (0 - 100) mg/dL'); | ||
await user.type(input, '0'); | ||
|
||
await waitFor(() => { | ||
|
@@ -212,7 +214,9 @@ describe('LabResultsForm', () => { | |
}); | ||
render(<LabResultsForm {...testProps} />); | ||
|
||
const input = await screen.findByLabelText(`Test Concept (0 - 100 mg/dL)`); | ||
const input = screen.getByRole('spinbutton', { | ||
name: /Test Concept/i, | ||
}); | ||
await user.type(input, '150'); | ||
|
||
const saveButton = screen.getByRole('button', { name: /Save and close/i }); | ||
|
@@ -246,7 +250,9 @@ describe('LabResultsForm', () => { | |
}); | ||
render(<LabResultsForm {...testProps} />); | ||
|
||
const input = await screen.findByLabelText(`Test Concept (0 - -- mg/dL)`); | ||
const input = screen.getByRole('spinbutton', { | ||
name: /Test Concept/i, | ||
}); | ||
await user.type(input, '-50'); | ||
|
||
const saveButton = screen.getByRole('button', { name: /Save and close/i }); | ||
|
@@ -270,7 +276,7 @@ describe('LabResultsForm', () => { | |
/>, | ||
); | ||
|
||
const input = await screen.findByLabelText(`Test Concept (0 - 100 mg/dL)`); | ||
const input = await screen.findByLabelText(`Test Concept (0 - 100) mg/dL`); | ||
await user.type(input, '50'); | ||
|
||
const saveButton = screen.getByRole('button', { name: /Save and close/i }); | ||
|
@@ -332,11 +338,11 @@ describe('LabResultsForm', () => { | |
render(<LabResultsForm {...testProps} />); | ||
|
||
// Normal input range | ||
const setMember1Input = screen.getByLabelText('Set Member (50 - 150 mg/dL)'); | ||
const setMember1Input = screen.getByLabelText('Set Member (50 - 150) mg/dL'); | ||
await user.type(setMember1Input, '50'); | ||
expect(screen.queryByText('Set Member must be between 50 and 150')).not.toBeInTheDocument(); | ||
|
||
const setMember2Input = screen.getByLabelText('Set Member 2 (5 - 30 mg/dL)'); | ||
const setMember2Input = screen.getByLabelText('Set Member 2 (5 - 30) mg/dL'); | ||
await user.type(setMember2Input, '10'); | ||
expect(screen.queryByText('Set Member must be between 5 and 30')).not.toBeInTheDocument(); | ||
|
||
|
@@ -453,7 +459,7 @@ describe('LabResultsForm', () => { | |
}); | ||
|
||
render(<LabResultsForm {...testProps} />); | ||
const concept1Input = await screen.findByLabelText('Set Member 1 (0 - 100 mg/dL)'); | ||
const concept1Input = await screen.findByLabelText('Set Member 1 (0 - 100) mg/dL'); | ||
await user.type(concept1Input, '100'); | ||
|
||
const submitButton = screen.getByRole('button', { name: 'Save and close' }); | ||
|
@@ -557,10 +563,10 @@ describe('LabResultsForm', () => { | |
}); | ||
|
||
render(<LabResultsForm {...testProps} />); | ||
const concept1Input = await screen.findByLabelText('Set Member 1 (0 - 100 mg/dL)'); | ||
const concept1Input = await screen.findByLabelText('Set Member 1 (0 - 100) mg/dL'); | ||
await user.type(concept1Input, '100'); | ||
|
||
const concept2Input = await screen.findByLabelText('Set Member 2 (0 - 80 mmol/L)'); | ||
const concept2Input = await screen.findByLabelText('Set Member 2 (0 - 80) mmol/L'); | ||
await user.type(concept2Input, '60'); | ||
|
||
const submitButton = screen.getByRole('button', { name: 'Save and close' }); | ||
|
@@ -638,7 +644,7 @@ describe('LabResultsForm', () => { | |
const user = userEvent.setup(); | ||
render(<LabResultsForm {...testProps} />); | ||
|
||
const input = await screen.findByLabelText(`Test Concept (0 - 100 mg/dL)`); | ||
const input = await screen.findByLabelText(`Test Concept (0 - 100) mg/dL`); | ||
await user.type(input, '150'); | ||
|
||
const saveButton = screen.getByRole('button', { name: /Save and close/i }); | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The current destructuring could throw an error if
datatype
is null/undefined, even with the nullish coalescing operator.