diff --git a/__mocks__/allergies.mock.ts b/__mocks__/allergies.mock.ts index 81a14fdfaa..b3d2700757 100644 --- a/__mocks__/allergies.mock.ts +++ b/__mocks__/allergies.mock.ts @@ -1011,58 +1011,69 @@ export const mockPatientAllergyResult = { }; export const mockAllergyResult = { - data: { - display: 'ARBs (angiotensin II receptor blockers)', - uuid: '90c17541-833d-419e-b5d3-bc06828bf95f', - allergen: { - allergenType: 'DRUG', - codedAllergen: { - display: 'ARBs (angiotensin II receptor blockers)', - uuid: '921fbd85-fa49-46c3-9ee1-77e093fd10a5', - }, - nonCodedAllergen: null, + display: 'ACE inhibitors', + uuid: 'dbba59ef-c8a5-4967-b20a-5761b1954f6d', + allergen: { + allergenType: 'DRUG', + codedAllergen: { + uuid: '162298AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', + display: 'ACE inhibitors', + links: [ + { + rel: 'self', + uri: 'http://localhost:8090/openmrslocalhost:8080/openmrs/ws/rest/v1/concept/162298AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', + }, + ], }, - severity: { - uuid: '1498AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', - display: 'Mild', - name: { - display: 'Mild', - uuid: '1738BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB', - name: 'Mild', - locale: 'en', - localePreferred: true, - conceptNameType: 'FULLY_SPECIFIED', + nonCodedAllergen: null, + }, + severity: { + uuid: '1498AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', + display: 'Mild', + links: [ + { + rel: 'self', + uri: 'http://localhost:8090/openmrslocalhost:8080/openmrs/ws/rest/v1/concept/1498AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', + }, + ], + }, + comment: null, + reactions: [ + { + reaction: { + uuid: '143264AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', + display: 'Cough', + links: [ + { + rel: 'self', + uri: 'http://localhost:8090/openmrslocalhost:8080/openmrs/ws/rest/v1/concept/143264AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', + }, + ], }, + reactionNonCoded: null, }, - comment: 'The patient is showing a mild reaction to the above allergens', - reactions: [ + ], + patient: { + uuid: '011cffa0-7383-45ef-962c-d1976718b8d4', + display: '103H22 - test test test', + links: [ { - reaction: { - uuid: '121677AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', - display: 'Mental status change', - name: { - display: 'Mental status change', - uuid: '127084BBBBBBBBBBBBBBBBBBBBBBBBBBBBBB', - name: 'Mental status change', - locale: 'en', - localePreferred: true, - conceptNameType: null, - }, - }, + rel: 'self', + uri: 'http://localhost:8090/openmrslocalhost:8080/openmrs/ws/rest/v1/patient/011cffa0-7383-45ef-962c-d1976718b8d4', }, ], - patient: { - uuid: '90f7f0b4-06a8-4a97-9678-e7a977f4b518', - display: '10010W - John Taylor', + }, + links: [ + { + rel: 'self', + uri: 'http://localhost:8090/openmrslocalhost:8080/openmrs/ws/rest/v1/patient/011cffa0-7383-45ef-962c-d1976718b8d4/allergy/dbba59ef-c8a5-4967-b20a-5761b1954f6d', }, - auditInfo: { - creator: { - uuid: '285f67ce-3d8b-4733-96e5-1e2235e8e804', - display: 'doc', - }, - dateChanged: '2020-01-03T07:05:12.000+0000', + { + rel: 'full', + uri: 'http://localhost:8090/openmrslocalhost:8080/openmrs/ws/rest/v1/patient/011cffa0-7383-45ef-962c-d1976718b8d4/allergy/dbba59ef-c8a5-4967-b20a-5761b1954f6d?v=full', }, - }, + ], + resourceVersion: '1.8', }; export const mockUpdatedAllergyResult = { @@ -1689,7 +1700,7 @@ export const mockFhirAllergyIntoleranceResponse = { ], }; -export const mockAllergenAndReactions = { +export const mockAllergensAndAllergicReactions = { drugAllergens: [ { uuid: '162298AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', display: 'ACE inhibitors' }, { uuid: '162299AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', display: 'ARBs (angiotensin II receptor blockers)' }, @@ -1730,7 +1741,7 @@ export const mockAllergenAndReactions = { { uuid: '162542AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', display: 'Adhesive tape' }, { uuid: '5622AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', display: 'Other' }, ], - allergyReaction: [ + allergicReactions: [ { uuid: '121677AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', display: 'Mental status change' }, { uuid: '121629AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', display: 'Anaemia' }, { uuid: '148888AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', display: 'Anaphylaxis' }, diff --git a/packages/esm-patient-allergies-app/src/allergies/allergies-detailed-summary.component.tsx b/packages/esm-patient-allergies-app/src/allergies/allergies-detailed-summary.component.tsx index ff18b3c0d2..f49c8a946e 100644 --- a/packages/esm-patient-allergies-app/src/allergies/allergies-detailed-summary.component.tsx +++ b/packages/esm-patient-allergies-app/src/allergies/allergies-detailed-summary.component.tsx @@ -2,7 +2,6 @@ import React from 'react'; import dayjs from 'dayjs'; import { useTranslation } from 'react-i18next'; import { CardHeader, EmptyState, ErrorState, launchPatientWorkspace } from '@openmrs/esm-patient-common-lib'; -import { useAllergies } from './allergy-intolerance.resource'; import Add16 from '@carbon/icons-react/es/add/16'; import { Button, @@ -18,19 +17,19 @@ import { TableRow, } from 'carbon-components-react'; import styles from './allergies-detailed-summary.scss'; +import { useAllergies } from './allergy-intolerance.resource'; import { patientAllergiesFormWorkspace } from '../constants'; interface AllergiesDetailedSummaryProps { patient: fhir.Patient; - showAddAllergy: boolean; + showAddAllergyButton: boolean; } -const AllergiesDetailedSummary: React.FC = ({ patient, showAddAllergy }) => { +const AllergiesDetailedSummary: React.FC = ({ patient, showAddAllergyButton }) => { const { t } = useTranslation(); const displayText = t('allergyIntolerances', 'allergy intolerances'); const headerTitle = t('allergies', 'Allergies'); - - const { data: allergies, isError, isLoading, isValidating } = useAllergies(patient.id); + const { allergies, isError, isLoading, isValidating } = useAllergies(patient.id); const launchAllergiesForm = React.useCallback(() => launchPatientWorkspace(patientAllergiesFormWorkspace), []); @@ -75,8 +74,8 @@ const AllergiesDetailedSummary: React.FC = ({ pat ), }, - recordedDate: dayjs(allergy.recordedDate).format('MMM-YYYY') ?? '-', - lastUpdated: dayjs(allergy.lastUpdated).format('DD-MMM-YYYY'), + recordedDate: dayjs(allergy.recordedDate).format('MMM-YYYY') ?? '--', + lastUpdated: allergy.lastUpdated ? dayjs(allergy.lastUpdated).format('DD-MMM-YYYY') : '--', })); }, [allergies]); @@ -87,7 +86,7 @@ const AllergiesDetailedSummary: React.FC = ({ pat
{isValidating ? : null} - {showAddAllergy && ( + {showAddAllergyButton && ( diff --git a/packages/esm-patient-allergies-app/src/allergies/allergies-detailed-summary.test.tsx b/packages/esm-patient-allergies-app/src/allergies/allergies-detailed-summary.test.tsx index 6ab15ca188..53294afd38 100644 --- a/packages/esm-patient-allergies-app/src/allergies/allergies-detailed-summary.test.tsx +++ b/packages/esm-patient-allergies-app/src/allergies/allergies-detailed-summary.test.tsx @@ -8,7 +8,7 @@ import AllergiesDetailedSummary from './allergies-detailed-summary.component'; const testProps = { patient: mockPatient, - showAddAllergy: false, + showAddAllergyButton: false, }; const mockOpenmrsFetch = openmrsFetch as jest.Mock; diff --git a/packages/esm-patient-allergies-app/src/allergies/allergies-form/allergies-form.test.tsx b/packages/esm-patient-allergies-app/src/allergies/allergies-form/allergies-form.test.tsx new file mode 100644 index 0000000000..ec649cc97b --- /dev/null +++ b/packages/esm-patient-allergies-app/src/allergies/allergies-form/allergies-form.test.tsx @@ -0,0 +1,131 @@ +import React from 'react'; +import userEvent from '@testing-library/user-event'; +import { screen, render, act } from '@testing-library/react'; +import { of } from 'rxjs/internal/observable/of'; +import { showNotification, showToast, useConfig } from '@openmrs/esm-framework'; +import { mockPatient } from '../../../../../__mocks__/patient.mock'; +import { mockAllergensAndAllergicReactions, mockAllergyResult } from '../../../../../__mocks__/allergies.mock'; +import { fetchAllergensAndAllergicReactions, saveAllergy } from './allergy-form.resource'; +import AllergyForm from './allergy-form.component'; + +const mockUseConfig = useConfig as jest.Mock; +const mockFetchAllergensAndAllergicReactions = fetchAllergensAndAllergicReactions as jest.Mock; +const mockSaveAllergy = saveAllergy as jest.Mock; +const mockShowNotification = showNotification as jest.Mock; +const mockShowToast = showToast as jest.Mock; + +jest.mock('./allergy-form.resource', () => ({ + fetchAllergensAndAllergicReactions: jest.fn(), + saveAllergy: jest.fn(), +})); + +describe('AllergiesForm: ', () => { + it('renders the Record Allergy form', () => { + mockFetchAllergensAndAllergicReactions.mockReturnValueOnce(of(mockAllergensAndAllergicReactions)); + + renderAllergyForm(); + + expect(screen.getByRole('heading', { name: /allergens and reactions/i })).toBeInTheDocument(); + expect(screen.getByRole('heading', { name: /select the allergens/i })).toBeInTheDocument(); + expect(screen.getByRole('heading', { name: /select the reactions/i })).toBeInTheDocument(); + expect(screen.getByRole('heading', { name: /severity and date of onset/i })).toBeInTheDocument(); + expect(screen.getByRole('heading', { name: /severity of worst reaction/i })).toBeInTheDocument(); + expect(screen.getByRole('heading', { name: /date and comments/i })).toBeInTheDocument(); + + const tabNames = [/drug/i, /food/i, /environmental/i]; + tabNames.map((tabName) => expect(screen.getByRole('tab', { name: tabName })).toBeInTheDocument()); + + expect(screen.getByRole('radio', { name: /ace inhibitors/i })).toBeInTheDocument(); + expect(screen.getByRole('radio', { name: /mild/i })).toBeInTheDocument(); + expect(screen.getByRole('radio', { name: /moderate/i })).toBeInTheDocument(); + expect(screen.getByRole('radio', { name: /severe/i })).toBeInTheDocument(); + expect(screen.getByRole('checkbox', { name: /anaemia/i })).toBeInTheDocument(); + expect(screen.getByRole('textbox', { name: /date of first onset/i })).toBeInTheDocument(); + expect(screen.getByRole('textbox', { name: /comments/i })).toBeInTheDocument(); + expect(screen.getByRole('button', { name: /discard/i })).toBeInTheDocument(); + expect(screen.getByRole('button', { name: /save and close/i })).toBeInTheDocument(); + expect(screen.getByRole('button', { name: /save and close/i })).toBeDisabled(); + }); + + describe('Form submission: ', () => { + it('renders a success notification after successful submission', async () => { + const promise = Promise.resolve(); + mockFetchAllergensAndAllergicReactions.mockReturnValueOnce(of(mockAllergensAndAllergicReactions)); + mockSaveAllergy.mockReturnValueOnce( + Promise.resolve({ data: mockAllergyResult, status: 201, statusText: 'Created' }), + ); + + renderAllergyForm(); + + userEvent.click(screen.getByRole('radio', { name: /ace inhibitors/i })); + userEvent.click(screen.getByRole('checkbox', { name: /cough/i })); + userEvent.click(screen.getByRole('radio', { name: /moderate/i })); + userEvent.type(screen.getByRole('textbox', { name: /date of first onset/i }), '02/01/2022'); + userEvent.click(screen.getByRole('button', { name: /save and close/i })); + + await act(() => promise); + + expect(mockShowToast).toHaveBeenCalledTimes(1); + expect(mockShowToast).toHaveBeenCalledWith({ + critical: true, + kind: 'success', + title: 'Allergy saved', + description: 'It is now visible on the Allergies page', + }); + }); + + it('renders an error notification upon an invalid submission', async () => { + const promise = Promise.resolve(); + mockFetchAllergensAndAllergicReactions.mockReturnValueOnce(of(mockAllergensAndAllergicReactions)); + mockSaveAllergy.mockRejectedValueOnce({ + message: 'Internal Server Error', + response: { + status: 500, + statusText: 'Internal Server Error', + }, + }); + + renderAllergyForm(); + + userEvent.click(screen.getByRole('radio', { name: /ace inhibitors/i })); + userEvent.click(screen.getByRole('checkbox', { name: /cough/i })); + userEvent.click(screen.getByRole('radio', { name: /moderate/i })); + userEvent.type(screen.getByRole('textbox', { name: /date of first onset/i }), '02/01/2022'); + userEvent.click(screen.getByRole('button', { name: /save and close/i })); + + await act(() => promise); + + expect(mockShowNotification).toHaveBeenCalledTimes(1); + expect(mockShowNotification).toHaveBeenCalledWith({ + critical: true, + description: 'Internal Server Error', + kind: 'error', + title: 'Error saving allergy', + }); + }); + }); +}); + +const testProps = { + closeWorkspace: () => {}, + isTablet: false, + patient: mockPatient, + patientUuid: mockPatient.id, +}; + +function renderAllergyForm() { + mockUseConfig.mockReturnValue({ + concepts: { + drugAllergenUuid: '162552AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', + environmentalAllergenUuid: '162554AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', + foodAllergenUuid: '162553AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', + mildReactionUuid: '1498AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', + moderateReactionUuid: '1499AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', + severeReactionUuid: '1500AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', + allergyReactionUuid: '162555AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', + otherConceptUuid: '5622AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', + }, + }); + + render(); +} diff --git a/packages/esm-patient-allergies-app/src/allergies/allergies-form/allergy-form.component.test.tsx b/packages/esm-patient-allergies-app/src/allergies/allergies-form/allergy-form.component.test.tsx deleted file mode 100644 index 5fdb76599e..0000000000 --- a/packages/esm-patient-allergies-app/src/allergies/allergies-form/allergy-form.component.test.tsx +++ /dev/null @@ -1,175 +0,0 @@ -import React from 'react'; -import { screen, render, act, waitFor, fireEvent } from '@testing-library/react'; -import AllergyForm from './allergy-form.component'; -import { mockPatient } from '../../../../../__mocks__/patient.mock'; -import { useConfig, showNotification } from '@openmrs/esm-framework'; -import { savePatientAllergy, fetchAllergensAndReaction } from './allergy-form.resource'; -import { of, throwError } from 'rxjs'; -import { mockAllergenAndReactions } from '../../../../../__mocks__/allergies.mock'; -import userEvent from '@testing-library/user-event'; - -const mockedCloseWorkspace = jest.fn(); -const mockedShowNotification = showNotification as jest.Mock; -const mockedUseConfig = useConfig as jest.Mock; - -window.HTMLElement.prototype.scrollIntoView = jest.fn(); -const mockSavePatientAllergy = savePatientAllergy as jest.Mock; -const mockFetchAllergensAndReaction = fetchAllergensAndReaction as jest.Mock; - -const mockConcepts = { - concepts: { - drugAllergenUuid: '162552AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', - environmentalAllergenUuid: '162554AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', - foodAllergenUuid: '162553AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', - mildReactionUuid: '1498AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', - moderateReactionUuid: '1499AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', - severeReactionUuid: '1500AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', - allergyReactionUuid: '162555AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', - otherConceptUuid: '5622AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', - }, -}; - -jest.mock('./allergy-form.resource', () => ({ - savePatientAllergy: jest.fn(), - fetchAllergensAndReaction: jest.fn(), -})); - -describe('', () => { - const renderAllergyForm = () => { - mockedUseConfig.mockReturnValue(mockConcepts); - mockFetchAllergensAndReaction.mockReturnValue(of(mockAllergenAndReactions)); - render( - , - ); - }; - - afterEach(() => { - jest.resetAllMocks(); - }); - - it('should display allergy form correctly', async () => { - renderAllergyForm(); - expect(screen.getByRole('tab', { name: /Drug/i })).toBeInTheDocument(); - expect(screen.getByRole('tab', { name: /Food/i })).toBeInTheDocument(); - expect(screen.getByRole('tab', { name: /Environmental/i })).toBeInTheDocument(); - - // Display drug allergens - expect(screen.getByRole('radio', { name: /ACE inhibitors/i })).toBeInTheDocument(); - expect(screen.getByRole('radio', { name: /Aspirin/i })).toBeInTheDocument(); - expect(screen.getByRole('radio', { name: /Cephalosporins/i })).toBeInTheDocument(); - expect(screen.getByRole('radio', { name: /Codeine/i })).toBeInTheDocument(); - - // Display food allergen - const foodAllergen = screen.getByRole('tab', { name: /Food/i }); - userEvent.click(foodAllergen); - - expect(screen.getByRole('radio', { name: /Beef/i })).toBeInTheDocument(); - expect(screen.getByRole('radio', { name: /Caffeine/i })).toBeInTheDocument(); - expect(screen.getByRole('radio', { name: /Chocolate/i })).toBeInTheDocument(); - expect(screen.getByRole('radio', { name: /Strawberries/i })).toBeInTheDocument(); - - const beefAllergen = screen.getByRole('radio', { name: /Beef/i }); - userEvent.click(beefAllergen); - expect(beefAllergen).toBeChecked(); - - // Should display other text box when other allergen is selected - const otherAllergen = screen.getByRole('radio', { name: /Other/i }); - userEvent.click(otherAllergen); - expect(otherAllergen).toBeChecked(); - expect(screen.getByRole('textbox', { name: /Please specify other allergen/i })).toBeInTheDocument(); - const otherAllergenTextInput = screen.getByRole('textbox', { name: /Please specify other allergen/i }); - userEvent.type(otherAllergenTextInput, 'Other Allergen Text'); - - // should display reaction checkboxes - expect(screen.getByRole('checkbox', { name: /Anaemia/i })).toBeInTheDocument(); - expect(screen.getByRole('checkbox', { name: /Anaphylaxis/i })).toBeInTheDocument(); - expect(screen.getByRole('checkbox', { name: /Headache/i })).toBeInTheDocument(); - expect(screen.getByRole('checkbox', { name: /Hypertension/i })).toBeInTheDocument(); - - // should be able to change patient allergy reaction - const anaemiaReaction = screen.getByRole('checkbox', { name: /Anaemia/i }); - expect(anaemiaReaction).not.toBeChecked(); - userEvent.click(anaemiaReaction); - expect(anaemiaReaction).toBeChecked(); - - // uncheck selected anaemia reaction - userEvent.click(anaemiaReaction); - expect(anaemiaReaction).not.toBeChecked(); - - // should display other reaction checkboxes - const otherReaction = screen.getByRole('checkbox', { name: /Other/i }); - userEvent.click(otherReaction); - expect(screen.getByLabelText(/Please specify other reaction/)).toBeInTheDocument(); - const otherReactionTextInput = screen.getByLabelText(/Please specify other reaction/); - userEvent.type(otherReactionTextInput, 'Other Reaction'); - - expect(screen.getByRole('radio', { name: /Mild/i })).toBeInTheDocument(); - expect(screen.getByRole('radio', { name: /Moderate/i })).toBeInTheDocument(); - expect(screen.getByRole('radio', { name: /Severe/i })).toBeInTheDocument(); - const severeReactionRadio = screen.getByRole('radio', { name: /Severe/i }); - userEvent.click(severeReactionRadio); - expect(severeReactionRadio).toBeChecked(); - - const commentText = screen.getByRole('textbox', { name: /Comments/i }); - userEvent.type(commentText, 'patient test comment'); - - const dateOfFirstOnset = screen.getByRole('textbox', { name: /Date of first onset/i }); - fireEvent.change(dateOfFirstOnset, new Date('2021-12-12').toISOString()); - - // Save patient allergy - mockSavePatientAllergy.mockReturnValue(Promise.resolve({ status: 201 })); - const saveButton = screen.getByRole('button', { name: /Save and Close/i }); - userEvent.click(saveButton); - expect(mockSavePatientAllergy).toHaveBeenCalledWith( - { - allergen: { - allergenType: 'ENVIRONMENT', - codedAllergen: { uuid: '5622AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' }, - nonCodedAllergen: 'Other Allergen Text', - }, - comment: 'patient test comment', - reactions: [{ reaction: { uuid: '5622AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' }, reactionNonCoded: 'Other Reaction' }], - severity: { uuid: '1500AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' }, - }, - '8673ee4f-e2ab-4077-ba55-4980f408773e', - expect.anything(), - ); - }); - - it('should display an error when loading the form fails', () => { - mockedUseConfig.mockReturnValue(mockConcepts); - mockFetchAllergensAndReaction.mockReturnValue(throwError('loading error')); - render( - , - ); - - expect(screen.getByText(/Allergy Form Error/i)).toBeInTheDocument(); - }); - - it('should display an error message when error occurs while saving patient allergy', async () => { - renderAllergyForm(); - const promise = Promise.resolve(); - mockSavePatientAllergy.mockReturnValue(Promise.reject({ status: 500 })); - const saveButton = screen.getByRole('button', { name: /Save and Close/i }); - userEvent.click(saveButton); - await waitFor(() => - expect(mockedShowNotification).toHaveBeenCalledWith({ - critical: true, - description: undefined, - kind: 'error', - title: 'Error saving allergy', - }), - ); - await act(() => promise); - }); -}); diff --git a/packages/esm-patient-allergies-app/src/allergies/allergies-form/allergy-form.component.tsx b/packages/esm-patient-allergies-app/src/allergies/allergies-form/allergy-form.component.tsx index d029421af7..0b799f0bdf 100644 --- a/packages/esm-patient-allergies-app/src/allergies/allergies-form/allergy-form.component.tsx +++ b/packages/esm-patient-allergies-app/src/allergies/allergies-form/allergy-form.component.tsx @@ -1,88 +1,50 @@ -import React from 'react'; -import { mutate } from 'swr'; -import styles from './allergy-form.component.scss'; -import AllergyFormTab from './allergy-form-tab.component'; -import { useTranslation } from 'react-i18next'; -import { fhirBaseUrl, showNotification, showToast, useConfig } from '@openmrs/esm-framework'; -import { AllergiesConfigObject } from '../../config-schema'; -import { fetchAllergensAndReaction, savePatientAllergy } from './allergy-form.resource'; -import { ErrorState } from '@openmrs/esm-patient-common-lib'; +import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { - SearchSkeleton, - TextInput, - TextArea, + Button, + ButtonSet, Checkbox, - Tabs, - Tab, DatePicker, DatePickerInput, + Form, + InlineNotification, RadioButton, - Button, RadioButtonGroup, - ButtonSet, + Row, + Tab, + Tabs, + TextArea, + TextInput, } from 'carbon-components-react'; -import { OpenMRSResource } from '../../types'; - -enum ActionTypes { - pending = 'pending', - resolved = 'resolved', - error = 'error', -} -interface Pending { - type: ActionTypes.pending; -} -interface Error { - type: ActionTypes.error; - payload: Error; -} - -interface Resolved { - type: ActionTypes.resolved; - payload: AllergyAndReactions; -} - -type Action = Pending | Error | Resolved; +import { useTranslation } from 'react-i18next'; +import { first } from 'rxjs/operators'; +import { mutate } from 'swr'; +import { + ExtensionSlot, + FetchResponse, + fhirBaseUrl, + showNotification, + showToast, + useConfig, +} from '@openmrs/esm-framework'; +import { Allergens, fetchAllergensAndAllergicReactions, saveAllergy, NewAllergy } from './allergy-form.resource'; +import styles from './allergy-form.scss'; -enum AllergenType { +enum AllergenTypes { FOOD = 'FOOD', DRUG = 'DRUG', ENVIRONMENT = 'ENVIRONMENT', } interface AllergyFormProps { + closeWorkspace: () => void; + isTablet: boolean; patient: fhir.Patient; patientUuid: string; - closeWorkspace(): void; - isTablet: boolean; } -interface AllergyAndReactions { - drugAllergens: Array; - foodAllergens: Array; - environmentalAllergens: Array; - allergyReaction: Array; -} - -interface PatientAllergenAndReactions { - status: 'pending' | 'resolved' | 'error'; - allergenAndReaction: AllergyAndReactions; - error?: null | Error; -} - -const formStatusReducer = (state: PatientAllergenAndReactions, action: Action): PatientAllergenAndReactions => { - switch (action.type) { - case 'pending': - return { ...state, status: action.type }; - case 'resolved': - return { allergenAndReaction: action.payload, status: action.type }; - case 'error': - return { ...state, error: action.payload, status: action.type }; - } -}; - -const AllergyForm: React.FC = ({ isTablet, closeWorkspace, patientUuid }) => { +const AllergyForm: React.FC = ({ closeWorkspace, isTablet, patientUuid }) => { const { t } = useTranslation(); - const { concepts } = useConfig() as AllergiesConfigObject; + const { concepts } = useConfig(); const { drugAllergenUuid, foodAllergenUuid, @@ -92,277 +54,292 @@ const AllergyForm: React.FC = ({ isTablet, closeWorkspace, pat severeReactionUuid, moderateReactionUuid, otherConceptUuid, - } = React.useMemo(() => concepts, [concepts]); - const [comment, setComment] = React.useState(); - const [patientReactions, setPatientReactions] = React.useState>([]); - const [selectedAllergen, setSelectedAllergen] = React.useState(); - const [severityOfReaction, setSeverityOfReaction] = React.useState(); - const [dateOfOnset, setDateOfOnset] = React.useState(); - const [{ status, allergenAndReaction, error }, dispatch] = React.useReducer(formStatusReducer, { - status: ActionTypes.pending, - allergenAndReaction: null, - }); - const [allergenType, setAllergenType] = React.useState(AllergenType.DRUG); - const [otherReaction, setOtherReaction] = React.useState(); - const [otherAllergen, setOtherAllergen] = React.useState(); + } = useMemo(() => concepts, [concepts]); + const patientState = useMemo(() => ({ patientUuid }), [patientUuid]); + const [allergens, setAllergens] = useState(null); + const [allergicReactions, setAllergicReactions] = useState>([]); + const [comment, setComment] = useState(''); + const [error, setError] = useState(null); + const [nonCodedAllergenType, setNonCodedAllergenType] = useState(''); + const [nonCodedAllergicReaction, setNonCodedAllergicReaction] = useState(''); + const [onsetDate, setOnsetDate] = useState(null); + const [selectedAllergen, setSelectedAllergen] = useState(''); + const [selectedAllergenType, setSelectedAllergenType] = useState(AllergenTypes.DRUG); + const [severityOfWorstReaction, setSeverityOfWorstReaction] = useState(''); + const allergenTypes = [t('drug', 'Drug'), t('food', 'Food'), t('environmental', 'Environmental')]; + const severityLevels = [t('mild', 'Mild'), t('moderate', 'Moderate'), t('severe', 'Severe')]; - React.useEffect(() => { - if (drugAllergenUuid && foodAllergenUuid && environmentalAllergenUuid) { - const sub = fetchAllergensAndReaction([ - drugAllergenUuid, - foodAllergenUuid, - environmentalAllergenUuid, - allergyReactionUuid, - ]).subscribe( - (data) => { - dispatch({ type: ActionTypes.resolved, payload: data }); - }, - (error) => { - dispatch({ type: ActionTypes.error, payload: error }); - }, - ); - return () => sub.unsubscribe(); - } - }, [drugAllergenUuid, environmentalAllergenUuid, foodAllergenUuid, allergyReactionUuid]); - - const handlePatientReactionChange = React.useCallback( - (value: boolean, id: string, event: React.ChangeEvent) => { - value - ? setPatientReactions((prevState) => [...prevState, id]) - : setPatientReactions((prevState) => prevState.filter((reaction) => reaction !== id)); - }, - [], - ); + useEffect(() => { + const allergenUuids = [drugAllergenUuid, foodAllergenUuid, environmentalAllergenUuid, allergyReactionUuid]; + fetchAllergensAndAllergicReactions(allergenUuids).pipe(first()).subscribe(setAllergens, setError); + }, [allergyReactionUuid, drugAllergenUuid, environmentalAllergenUuid, foodAllergenUuid]); - const handleAllergenTypeChange = React.useCallback((index: number) => { + const handleTabChange = (index: number) => { switch (index) { case 0: - setAllergenType(AllergenType.DRUG); + setSelectedAllergenType(AllergenTypes.DRUG); + break; case 1: - setAllergenType(AllergenType.FOOD); + setSelectedAllergenType(AllergenTypes.FOOD); + break; case 2: - setAllergenType(AllergenType.ENVIRONMENT); + setSelectedAllergenType(AllergenTypes.ENVIRONMENT); + break; } + }; + + const handleAllergicReactionChange = useCallback((value: boolean, id: string) => { + value + ? setAllergicReactions((prevState) => [...prevState, id]) + : setAllergicReactions((prevState) => prevState.filter((reaction) => reaction !== id)); }, []); - const handleSavePatientAllergy = React.useCallback(() => { - const allergyPayload = { - allergen: - selectedAllergen === otherConceptUuid - ? { - allergenType: allergenType, - codedAllergen: { - uuid: selectedAllergen, - }, - nonCodedAllergen: otherAllergen, - } - : { - allergenType: allergenType, - codedAllergen: { - uuid: selectedAllergen, - }, - }, - severity: { - uuid: severityOfReaction, - }, - comment: comment, - reactions: patientReactions?.map((reaction) => { - return reaction === otherConceptUuid - ? { reaction: { uuid: reaction }, reactionNonCoded: otherReaction } - : { reaction: { uuid: reaction } }; - }), - }; + const handleSubmit = useCallback( + (event: React.SyntheticEvent) => { + event.preventDefault(); - const ac = new AbortController(); + let payload: NewAllergy = { + allergen: + selectedAllergen === otherConceptUuid + ? { + allergenType: selectedAllergenType, + codedAllergen: { + uuid: selectedAllergen, + }, + nonCodedAllergen: nonCodedAllergenType, + } + : { + allergenType: selectedAllergenType, + codedAllergen: { + uuid: selectedAllergen, + }, + }, + severity: { + uuid: severityOfWorstReaction, + }, + comment: comment, + reactions: allergicReactions?.map((reaction) => { + return reaction === otherConceptUuid + ? { reaction: { uuid: reaction }, reactionNonCoded: nonCodedAllergicReaction } + : { reaction: { uuid: reaction } }; + }), + }; - savePatientAllergy(allergyPayload, patientUuid, ac).then( - (response) => { - if (response.status === 201) { - closeWorkspace(); + const abortController = new AbortController(); + saveAllergy(payload, patientUuid, abortController) + .then( + (response: FetchResponse) => { + if (response.status === 201) { + closeWorkspace(); - showToast({ - critical: true, - kind: 'success', - title: t('allergySaved', 'Allergy saved'), - description: t('allergyNowVisible', 'It is now visible on the Allergies page'), - }); + showToast({ + critical: true, + kind: 'success', + title: t('allergySaved', 'Allergy saved'), + description: t('allergyNowVisible', 'It is now visible on the Allergies page'), + }); - mutate(`${fhirBaseUrl}/AllergyIntolerance?patient=${patientUuid}`); - } - }, - (err) => { - dispatch({ type: ActionTypes.error, payload: err }); - showNotification({ - title: t('allergySaveError', 'Error saving allergy'), - kind: 'error', - critical: true, - description: err?.message, - }); - }, - ); - return () => ac.abort(); - }, [ - selectedAllergen, - otherConceptUuid, - allergenType, - otherAllergen, - severityOfReaction, - comment, - patientReactions, - patientUuid, - otherReaction, - closeWorkspace, - t, - ]); + mutate(`${fhirBaseUrl}/AllergyIntolerance?patient=${patientUuid}`); + } + }, + (err) => { + showNotification({ + title: t('allergySaveError', 'Error saving allergy'), + kind: 'error', + critical: true, + description: err?.message, + }); + }, + ) + .finally(() => abortController.abort()); + }, + [ + selectedAllergen, + otherConceptUuid, + selectedAllergenType, + nonCodedAllergenType, + severityOfWorstReaction, + comment, + allergicReactions, + patientUuid, + nonCodedAllergicReaction, + closeWorkspace, + t, + ], + ); return ( -
- {status === ActionTypes.pending && } - {status === ActionTypes.resolved && ( -
-
-
-
- {t('allergenAndReaction', 'Allergen and reactions')} -
-
-
-
- {t('selectTheAllergens', 'Select the allergens')} -
- - - - - - - - - - - - {selectedAllergen === otherConceptUuid && ( - setOtherAllergen(event.target.value)} - placeholder={t('enterOtherReaction', 'Type in other Allergen')} - /> - )} -
-
-
- {t('selectTheReactions', 'Select the reactions')} -
-
- {allergenAndReaction?.allergyReaction?.map((reaction, index) => ( - - ))} -
- {patientReactions.includes(otherConceptUuid) && ( - setOtherReaction(event.target.value)} - placeholder={t('enterOtherReaction', 'Type in other reaction')} - /> - )} -
+
+ {isTablet ? ( + + + + ) : null} +
+

{t('allergensAndReactions', 'Allergens and reactions')}

+ {error ? ( + + ) : null} +
+
+

{t('selectAllergens', 'Select the allergens')}

+ + {allergenTypes.map((allergenType, index) => { + const allergenCategory = allergenType.toLowerCase() + 'Allergens'; + return ( + +
+ setSelectedAllergen(event.toString())} + valueSelected={selectedAllergen} + > + {allergens?.[allergenCategory]?.map((allergen) => ( + + ))} + +
+
+ ); + })} +
+ {selectedAllergen === otherConceptUuid ? ( +
+ setNonCodedAllergenType(event.target.value)} + placeholder={t('typeAllergenName', 'Please type in the name of the allergen')} + />
+ ) : null} +
+
+

{t('selectReactions', 'Select the reactions')}

+
+ {allergens?.allergicReactions?.map((reaction, index) => ( + + ))}
-
-
- {t('severityAndDateOfOnset', 'Severity and date of onset')} -
-
-
-
- {t('severityOfWorstReaction', 'Severity of worst reaction')} -
- setSeverityOfReaction(event.toString())} - name="severityOfWorstReaction" - valueSelected={severityOfReaction} - > - - - - -
-
-
{t('dateAndComments', 'Date and comments')}
- - setDateOfOnset(event.target.valueAsDate)} - /> - -