diff --git a/package.json b/package.json index fe4fc954d8..87dfc2e87d 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,7 @@ "private": false, "license": "MIT", "dependencies": { - "@hospitalrun/components": "^1.4.0", + "@hospitalrun/components": "^1.5.0", "@reduxjs/toolkit": "~1.3.0", "@types/escape-string-regexp": "~2.0.1", "@types/pouchdb-find": "~6.3.4", diff --git a/src/HospitalRun.tsx b/src/HospitalRun.tsx index 199e4be740..8b6ba566f5 100644 --- a/src/HospitalRun.tsx +++ b/src/HospitalRun.tsx @@ -2,25 +2,20 @@ import React from 'react' import { Switch, Route } from 'react-router-dom' import { useSelector } from 'react-redux' import { Toaster } from '@hospitalrun/components' -import Appointments from 'scheduling/appointments/Appointments' -import NewAppointment from 'scheduling/appointments/new/NewAppointment' -import EditAppointment from 'scheduling/appointments/edit/EditAppointment' -import ViewAppointment from 'scheduling/appointments/view/ViewAppointment' import Breadcrumbs from 'breadcrumbs/Breadcrumbs' import { ButtonBarProvider } from 'page-header/ButtonBarProvider' import ButtonToolBar from 'page-header/ButtonToolBar' import Labs from 'labs/Labs' import Sidebar from './components/Sidebar' -import Permissions from './model/Permissions' import Dashboard from './dashboard/Dashboard' import { RootState } from './store' import Navbar from './components/Navbar' import PrivateRoute from './components/PrivateRoute' import Patients from './patients/Patients' +import Appointments from './scheduling/appointments/Appointments' const HospitalRun = () => { const { title } = useSelector((state: RootState) => state.title) - const { permissions } = useSelector((state: RootState) => state.user) const { sidebarCollapsed } = useSelector((state: RootState) => state.components) return ( @@ -44,34 +39,7 @@ const HospitalRun = () => {
- - - - - + diff --git a/src/__tests__/HospitalRun.test.tsx b/src/__tests__/HospitalRun.test.tsx index 737a42f61c..4b5f9d9617 100644 --- a/src/__tests__/HospitalRun.test.tsx +++ b/src/__tests__/HospitalRun.test.tsx @@ -8,16 +8,8 @@ import configureMockStore from 'redux-mock-store' import { Toaster } from '@hospitalrun/components' import { act } from 'react-dom/test-utils' import Dashboard from 'dashboard/Dashboard' -import Appointments from 'scheduling/appointments/Appointments' -import NewAppointment from 'scheduling/appointments/new/NewAppointment' -import EditAppointment from 'scheduling/appointments/edit/EditAppointment' -import { addBreadcrumbs } from 'breadcrumbs/breadcrumbs-slice' import ViewLabs from 'labs/ViewLabs' import LabRepository from 'clients/db/LabRepository' -import PatientRepository from '../clients/db/PatientRepository' -import AppointmentRepository from '../clients/db/AppointmentRepository' -import Patient from '../model/Patient' -import Appointment from '../model/Appointment' import HospitalRun from '../HospitalRun' import Permissions from '../model/Permissions' @@ -25,191 +17,6 @@ const mockStore = configureMockStore([thunk]) describe('HospitalRun', () => { describe('routing', () => { - describe('/appointments', () => { - it('should render the appointments screen when /appointments is accessed', async () => { - const store = mockStore({ - title: 'test', - user: { permissions: [Permissions.ReadAppointments] }, - appointments: { appointments: [] }, - breadcrumbs: { breadcrumbs: [] }, - components: { sidebarCollapsed: false }, - }) - - const wrapper = mount( - - - - - , - ) - - await act(async () => { - wrapper.update() - }) - - expect(wrapper.find(Appointments)).toHaveLength(1) - - expect(store.getActions()).toContainEqual( - addBreadcrumbs([ - { i18nKey: 'scheduling.appointments.label', location: '/appointments' }, - { i18nKey: 'dashboard.label', location: '/' }, - ]), - ) - }) - - it('should render the Dashboard when the user does not have read appointment privileges', () => { - const wrapper = mount( - - - - - , - ) - - expect(wrapper.find(Dashboard)).toHaveLength(1) - }) - }) - - describe('/appointments/new', () => { - it('should render the new appointment screen when /appointments/new is accessed', async () => { - const store = mockStore({ - title: 'test', - user: { permissions: [Permissions.WriteAppointments] }, - breadcrumbs: { breadcrumbs: [] }, - components: { sidebarCollapsed: false }, - }) - - const wrapper = mount( - - - - - , - ) - - wrapper.update() - - expect(wrapper.find(NewAppointment)).toHaveLength(1) - expect(store.getActions()).toContainEqual( - addBreadcrumbs([ - { i18nKey: 'scheduling.appointments.label', location: '/appointments' }, - { i18nKey: 'scheduling.appointments.new', location: '/appointments/new' }, - { i18nKey: 'dashboard.label', location: '/' }, - ]), - ) - }) - - it('should render the Dashboard when the user does not have read appointment privileges', () => { - const wrapper = mount( - - - - - , - ) - - expect(wrapper.find(Dashboard)).toHaveLength(1) - }) - }) - - describe('/appointments/edit/:id', () => { - it('should render the edit appointment screen when /appointments/edit/:id is accessed', () => { - const appointment = { - id: '123', - patientId: '456', - } as Appointment - - const patient = { - id: '456', - } as Patient - - jest.spyOn(AppointmentRepository, 'find').mockResolvedValue(appointment) - jest.spyOn(PatientRepository, 'find').mockResolvedValue(patient) - - const store = mockStore({ - title: 'test', - user: { permissions: [Permissions.WriteAppointments, Permissions.ReadAppointments] }, - appointment: { appointment, patient: {} as Patient }, - breadcrumbs: { breadcrumbs: [] }, - components: { sidebarCollapsed: false }, - }) - - const wrapper = mount( - - - - - , - ) - - expect(wrapper.find(EditAppointment)).toHaveLength(1) - - expect(store.getActions()).toContainEqual( - addBreadcrumbs([ - { i18nKey: 'scheduling.appointments.label', location: '/appointments' }, - { text: '123', location: '/appointments/123' }, - { - i18nKey: 'scheduling.appointments.editAppointment', - location: '/appointments/edit/123', - }, - { i18nKey: 'dashboard.label', location: '/' }, - ]), - ) - }) - - it('should render the Dashboard when the user does not have read appointment privileges', () => { - const wrapper = mount( - - - - - , - ) - - expect(wrapper.find(Dashboard)).toHaveLength(1) - }) - - it('should render the Dashboard when the user does not have write appointment privileges', () => { - const wrapper = mount( - - - - - , - ) - - expect(wrapper.find(Dashboard)).toHaveLength(1) - }) - }) - describe('/labs', () => { it('should render the Labs component when /labs is accessed', async () => { jest.spyOn(LabRepository, 'findAll').mockResolvedValue([]) diff --git a/src/__tests__/labs/ViewLab.test.tsx b/src/__tests__/labs/ViewLab.test.tsx index b5cbd154e2..bbd6dba8fc 100644 --- a/src/__tests__/labs/ViewLab.test.tsx +++ b/src/__tests__/labs/ViewLab.test.tsx @@ -59,7 +59,7 @@ describe('View Labs', () => { lab, patient: mockPatient, error, - status: Object.keys(error).length > 0 ? 'error' : 'success', + status: Object.keys(error).length > 0 ? 'error' : 'completed', }, }) diff --git a/src/__tests__/labs/lab-slice.test.ts b/src/__tests__/labs/lab-slice.test.ts index 0a8a312d96..6296f36b7e 100644 --- a/src/__tests__/labs/lab-slice.test.ts +++ b/src/__tests__/labs/lab-slice.test.ts @@ -47,7 +47,7 @@ describe('lab slice', () => { fetchLabSuccess({ lab: expectedLab, patient: expectedPatient }), ) - expect(labStore.status).toEqual('success') + expect(labStore.status).toEqual('completed') expect(labStore.lab).toEqual(expectedLab) expect(labStore.patient).toEqual(expectedPatient) }) @@ -67,7 +67,7 @@ describe('lab slice', () => { const labStore = labSlice(undefined, updateLabSuccess(expectedLab)) - expect(labStore.status).toEqual('success') + expect(labStore.status).toEqual('completed') expect(labStore.lab).toEqual(expectedLab) }) }) @@ -86,7 +86,7 @@ describe('lab slice', () => { const labStore = labSlice(undefined, requestLabSuccess(expectedLab)) - expect(labStore.status).toEqual('success') + expect(labStore.status).toEqual('completed') expect(labStore.lab).toEqual(expectedLab) }) }) @@ -114,7 +114,7 @@ describe('lab slice', () => { const labStore = labSlice(undefined, completeLabSuccess(expectedLab)) - expect(labStore.status).toEqual('success') + expect(labStore.status).toEqual('completed') expect(labStore.lab).toEqual(expectedLab) }) }) @@ -142,7 +142,7 @@ describe('lab slice', () => { const labStore = labSlice(undefined, cancelLabSuccess(expectedLab)) - expect(labStore.status).toEqual('success') + expect(labStore.status).toEqual('completed') expect(labStore.lab).toEqual(expectedLab) }) }) diff --git a/src/__tests__/patients/GeneralInformation.test.tsx b/src/__tests__/patients/GeneralInformation.test.tsx index cfd0c525a9..368079a078 100644 --- a/src/__tests__/patients/GeneralInformation.test.tsx +++ b/src/__tests__/patients/GeneralInformation.test.tsx @@ -18,12 +18,8 @@ describe('Error handling', () => { phoneNumber: 'phone number message', email: 'email message', } - const history = createMemoryHistory() - const wrapper = mount( - - - , - ) + + const wrapper = mount() wrapper.update() const errorMessage = wrapper.find(Alert) diff --git a/src/__tests__/scheduling/appointments/AppointmentDetailForm.test.tsx b/src/__tests__/scheduling/appointments/AppointmentDetailForm.test.tsx index fb993888a5..27828f2ce3 100644 --- a/src/__tests__/scheduling/appointments/AppointmentDetailForm.test.tsx +++ b/src/__tests__/scheduling/appointments/AppointmentDetailForm.test.tsx @@ -4,12 +4,42 @@ import { mount, ReactWrapper } from 'enzyme' import AppointmentDetailForm from 'scheduling/appointments/AppointmentDetailForm' import Appointment from 'model/Appointment' import { roundToNearestMinutes, addMinutes } from 'date-fns' -import { Typeahead } from '@hospitalrun/components' +import { Typeahead, Alert } from '@hospitalrun/components' import PatientRepository from 'clients/db/PatientRepository' import Patient from 'model/Patient' import { act } from '@testing-library/react' describe('AppointmentDetailForm', () => { + describe('Error handling', () => { + it('should display errors', () => { + const error = { + message: 'some message', + patient: 'patient message', + startDateTime: 'start date time message', + } + + const wrapper = mount( + , + ) + wrapper.update() + + const errorMessage = wrapper.find(Alert) + const patientTypeahead = wrapper.find(Typeahead) + const startDateInput = wrapper.findWhere((w: any) => w.prop('name') === 'startDate') + expect(errorMessage).toBeTruthy() + expect(errorMessage.prop('message')).toMatch(error.message) + expect(patientTypeahead.prop('isInvalid')).toBeTruthy() + expect(patientTypeahead.prop('feedback')).toEqual(error.patient) + expect(startDateInput.prop('isInvalid')).toBeTruthy() + expect(startDateInput.prop('feedback')).toEqual(error.startDateTime) + }) + }) + describe('layout - editable', () => { let wrapper: ReactWrapper const expectedAppointment = { diff --git a/src/__tests__/scheduling/appointments/Appointments.test.tsx b/src/__tests__/scheduling/appointments/Appointments.test.tsx index d7f2feea05..c6acd3459a 100644 --- a/src/__tests__/scheduling/appointments/Appointments.test.tsx +++ b/src/__tests__/scheduling/appointments/Appointments.test.tsx @@ -1,92 +1,207 @@ import '../../../__mocks__/matchMediaMock' import React from 'react' -import { mount } from 'enzyme' -import { MemoryRouter } from 'react-router-dom' import { Provider } from 'react-redux' -import Appointments from 'scheduling/appointments/Appointments' +import { MemoryRouter } from 'react-router' import configureMockStore from 'redux-mock-store' +import { mount } from 'enzyme' import thunk from 'redux-thunk' -import { Calendar } from '@hospitalrun/components' -import { act } from '@testing-library/react' -import PatientRepository from 'clients/db/PatientRepository' -import { mocked } from 'ts-jest/utils' -import Patient from 'model/Patient' -import * as ButtonBarProvider from 'page-header/ButtonBarProvider' -import AppointmentRepository from 'clients/db/AppointmentRepository' -import Appointment from 'model/Appointment' -import * as titleUtil from '../../../page-header/useTitle' - -describe('Appointments', () => { - const expectedAppointments = [ - { - id: '123', - rev: '1', - patientId: '1234', - startDateTime: new Date().toISOString(), - endDateTime: new Date().toISOString(), - location: 'location', - reason: 'reason', - }, - ] as Appointment[] - - const setup = async () => { - jest.spyOn(AppointmentRepository, 'findAll').mockResolvedValue(expectedAppointments) - jest.spyOn(PatientRepository, 'find') - const mockedPatientRepository = mocked(PatientRepository, true) - mockedPatientRepository.find.mockResolvedValue({ - id: '123', - fullName: 'patient full name', - } as Patient) - const mockStore = configureMockStore([thunk]) - return mount( - +import { act } from 'react-dom/test-utils' +import NewAppointment from 'scheduling/appointments/new/NewAppointment' +import EditAppointment from 'scheduling/appointments/edit/EditAppointment' +import ViewAppointments from 'scheduling/appointments/ViewAppointments' +import Permissions from '../../../model/Permissions' +import HospitalRun from '../../../HospitalRun' +import { addBreadcrumbs } from '../../../breadcrumbs/breadcrumbs-slice' +import Dashboard from '../../../dashboard/Dashboard' +import PatientRepository from '../../../clients/db/PatientRepository' +import AppointmentRepository from '../../../clients/db/AppointmentRepository' +import Patient from '../../../model/Patient' +import Appointment from '../../../model/Appointment' + +const mockStore = configureMockStore([thunk]) + +describe('/appointments', () => { + it('should render the appointments screen when /appointments is accessed', async () => { + const store = mockStore({ + title: 'test', + user: { permissions: [Permissions.ReadAppointments] }, + appointments: { appointments: [] }, + breadcrumbs: { breadcrumbs: [] }, + components: { sidebarCollapsed: false }, + }) + + const wrapper = mount( + - + , ) - } - it('should use "Appointments" as the header', async () => { - jest.spyOn(titleUtil, 'default') await act(async () => { - await setup() + wrapper.update() }) - expect(titleUtil.default).toHaveBeenCalledWith('scheduling.appointments.label') + + expect(wrapper.find(ViewAppointments)).toHaveLength(1) + + expect(store.getActions()).toContainEqual( + addBreadcrumbs([ + { i18nKey: 'scheduling.appointments.label', location: '/appointments' }, + { i18nKey: 'dashboard.label', location: '/' }, + ]), + ) }) - it('should add a "New Appointment" button to the button tool bar', async () => { - jest.spyOn(ButtonBarProvider, 'useButtonToolbarSetter') - const setButtonToolBarSpy = jest.fn() - mocked(ButtonBarProvider).useButtonToolbarSetter.mockReturnValue(setButtonToolBarSpy) + it('should render the Dashboard when the user does not have read appointment privileges', () => { + const wrapper = mount( + + + + + , + ) - await act(async () => { - await setup() + expect(wrapper.find(Dashboard)).toHaveLength(1) + }) +}) + +describe('/appointments/new', () => { + it('should render the new appointment screen when /appointments/new is accessed', async () => { + const store = mockStore({ + title: 'test', + user: { permissions: [Permissions.WriteAppointments] }, + appointment: {}, + breadcrumbs: { breadcrumbs: [] }, + components: { sidebarCollapsed: false }, }) - const actualButtons: React.ReactNode[] = setButtonToolBarSpy.mock.calls[0][0] - expect((actualButtons[0] as any).props.children).toEqual('scheduling.appointments.new') + const wrapper = mount( + + + + + , + ) + + wrapper.update() + + expect(wrapper.find(NewAppointment)).toHaveLength(1) + expect(store.getActions()).toContainEqual( + addBreadcrumbs([ + { i18nKey: 'scheduling.appointments.label', location: '/appointments' }, + { i18nKey: 'scheduling.appointments.new', location: '/appointments/new' }, + { i18nKey: 'dashboard.label', location: '/' }, + ]), + ) }) - it('should render a calendar with the proper events', async () => { - let wrapper: any - await act(async () => { - wrapper = await setup() + it('should render the Dashboard when the user does not have read appointment privileges', () => { + const wrapper = mount( + + + + + , + ) + + expect(wrapper.find(Dashboard)).toHaveLength(1) + }) +}) + +describe('/appointments/edit/:id', () => { + it('should render the edit appointment screen when /appointments/edit/:id is accessed', () => { + const appointment = { + id: '123', + patientId: '456', + } as Appointment + + const patient = { + id: '456', + } as Patient + + jest.spyOn(AppointmentRepository, 'find').mockResolvedValue(appointment) + jest.spyOn(PatientRepository, 'find').mockResolvedValue(patient) + + const store = mockStore({ + title: 'test', + user: { permissions: [Permissions.WriteAppointments, Permissions.ReadAppointments] }, + appointment: { appointment, patient: {} as Patient }, + breadcrumbs: { breadcrumbs: [] }, + components: { sidebarCollapsed: false }, }) - wrapper.update() - const expectedEvents = [ - { - id: expectedAppointments[0].id, - start: new Date(expectedAppointments[0].startDateTime), - end: new Date(expectedAppointments[0].endDateTime), - title: 'patient full name', - allDay: false, - }, - ] - - const calendar = wrapper.find(Calendar) - expect(calendar).toHaveLength(1) - expect(calendar.prop('events')).toEqual(expectedEvents) + const wrapper = mount( + + + + + , + ) + + expect(wrapper.find(EditAppointment)).toHaveLength(1) + + expect(store.getActions()).toContainEqual( + addBreadcrumbs([ + { i18nKey: 'scheduling.appointments.label', location: '/appointments' }, + { text: '123', location: '/appointments/123' }, + { + i18nKey: 'scheduling.appointments.editAppointment', + location: '/appointments/edit/123', + }, + { i18nKey: 'dashboard.label', location: '/' }, + ]), + ) + }) + + it('should render the Dashboard when the user does not have read appointment privileges', () => { + const wrapper = mount( + + + + + , + ) + + expect(wrapper.find(Dashboard)).toHaveLength(1) + }) + + it('should render the Dashboard when the user does not have write appointment privileges', () => { + const wrapper = mount( + + + + + , + ) + + expect(wrapper.find(Dashboard)).toHaveLength(1) }) }) diff --git a/src/__tests__/scheduling/appointments/ViewAppointments.test.tsx b/src/__tests__/scheduling/appointments/ViewAppointments.test.tsx new file mode 100644 index 0000000000..2f5db67cf3 --- /dev/null +++ b/src/__tests__/scheduling/appointments/ViewAppointments.test.tsx @@ -0,0 +1,92 @@ +import '../../../__mocks__/matchMediaMock' +import React from 'react' +import { mount } from 'enzyme' +import { MemoryRouter } from 'react-router-dom' +import { Provider } from 'react-redux' +import ViewAppointments from 'scheduling/appointments/ViewAppointments' +import configureMockStore from 'redux-mock-store' +import thunk from 'redux-thunk' +import { Calendar } from '@hospitalrun/components' +import { act } from '@testing-library/react' +import PatientRepository from 'clients/db/PatientRepository' +import { mocked } from 'ts-jest/utils' +import Patient from 'model/Patient' +import * as ButtonBarProvider from 'page-header/ButtonBarProvider' +import AppointmentRepository from 'clients/db/AppointmentRepository' +import Appointment from 'model/Appointment' +import * as titleUtil from '../../../page-header/useTitle' + +describe('ViewAppointments', () => { + const expectedAppointments = [ + { + id: '123', + rev: '1', + patientId: '1234', + startDateTime: new Date().toISOString(), + endDateTime: new Date().toISOString(), + location: 'location', + reason: 'reason', + }, + ] as Appointment[] + + const setup = async () => { + jest.spyOn(AppointmentRepository, 'findAll').mockResolvedValue(expectedAppointments) + jest.spyOn(PatientRepository, 'find') + const mockedPatientRepository = mocked(PatientRepository, true) + mockedPatientRepository.find.mockResolvedValue({ + id: '123', + fullName: 'patient full name', + } as Patient) + const mockStore = configureMockStore([thunk]) + return mount( + + + + + , + ) + } + + it('should use "Appointments" as the header', async () => { + jest.spyOn(titleUtil, 'default') + await act(async () => { + await setup() + }) + expect(titleUtil.default).toHaveBeenCalledWith('scheduling.appointments.label') + }) + + it('should add a "New Appointment" button to the button tool bar', async () => { + jest.spyOn(ButtonBarProvider, 'useButtonToolbarSetter') + const setButtonToolBarSpy = jest.fn() + mocked(ButtonBarProvider).useButtonToolbarSetter.mockReturnValue(setButtonToolBarSpy) + + await act(async () => { + await setup() + }) + + const actualButtons: React.ReactNode[] = setButtonToolBarSpy.mock.calls[0][0] + expect((actualButtons[0] as any).props.children).toEqual('scheduling.appointments.new') + }) + + it('should render a calendar with the proper events', async () => { + let wrapper: any + await act(async () => { + wrapper = await setup() + }) + wrapper.update() + + const expectedEvents = [ + { + id: expectedAppointments[0].id, + start: new Date(expectedAppointments[0].startDateTime), + end: new Date(expectedAppointments[0].endDateTime), + title: 'patient full name', + allDay: false, + }, + ] + + const calendar = wrapper.find(Calendar) + expect(calendar).toHaveLength(1) + expect(calendar.prop('events')).toEqual(expectedEvents) + }) +}) diff --git a/src/__tests__/scheduling/appointments/appointment-slice.test.ts b/src/__tests__/scheduling/appointments/appointment-slice.test.ts index ccc092b490..50c8ecedbd 100644 --- a/src/__tests__/scheduling/appointments/appointment-slice.test.ts +++ b/src/__tests__/scheduling/appointments/appointment-slice.test.ts @@ -5,15 +5,19 @@ import AppointmentRepository from 'clients/db/AppointmentRepository' import { mocked } from 'ts-jest/utils' import PatientRepository from 'clients/db/PatientRepository' import Patient from 'model/Patient' +import { subDays } from 'date-fns' + import appointment, { fetchAppointmentStart, fetchAppointmentSuccess, fetchAppointment, createAppointmentStart, createAppointmentSuccess, + createAppointmentError, createAppointment, updateAppointmentStart, updateAppointmentSuccess, + updateAppointmentError, updateAppointment, deleteAppointment, deleteAppointmentStart, @@ -26,14 +30,14 @@ describe('appointment slice', () => { const appointmentStore = appointment(undefined, {} as AnyAction) expect(appointmentStore.appointment).toEqual({} as Appointment) - expect(appointmentStore.isLoading).toBeFalsy() + expect(appointmentStore.status).toEqual('loading') }) it('should handle the CREATE_APPOINTMENT_START action', () => { const appointmentStore = appointment(undefined, { type: createAppointmentStart.type, }) - expect(appointmentStore.isLoading).toBeTruthy() + expect(appointmentStore.status).toEqual('loading') }) it('should handle the CREATE_APPOINTMENT_SUCCESS action', () => { @@ -41,7 +45,7 @@ describe('appointment slice', () => { type: createAppointmentSuccess.type, }) - expect(appointmentStore.isLoading).toBeFalsy() + expect(appointmentStore.status).toEqual('completed') }) it('should handle the UPDATE_APPOINTMENT_START action', () => { @@ -49,7 +53,7 @@ describe('appointment slice', () => { type: updateAppointmentStart.type, }) - expect(appointmentStore.isLoading).toBeTruthy() + expect(appointmentStore.status).toEqual('loading') }) it('should handle the UPDATE_APPOINTMENT_SUCCESS action', () => { @@ -68,7 +72,7 @@ describe('appointment slice', () => { }, }) - expect(appointmentStore.isLoading).toBeFalsy() + expect(appointmentStore.status).toEqual('completed') expect(appointmentStore.appointment).toEqual(expectedAppointment) }) @@ -77,7 +81,7 @@ describe('appointment slice', () => { type: fetchAppointmentStart.type, }) - expect(appointmentStore.isLoading).toBeTruthy() + expect(appointmentStore.status).toEqual('loading') }) it('should handle the FETCH_APPOINTMENT_SUCCESS action', () => { const expectedAppointment = { @@ -96,7 +100,7 @@ describe('appointment slice', () => { payload: { appointment: expectedAppointment, patient: expectedPatient }, }) - expect(appointmentStore.isLoading).toBeFalsy() + expect(appointmentStore.status).toEqual('completed') expect(appointmentStore.appointment).toEqual(expectedAppointment) expect(appointmentStore.patient).toEqual(expectedPatient) }) @@ -106,7 +110,7 @@ describe('appointment slice', () => { type: deleteAppointmentStart.type, }) - expect(appointmentStore.isLoading).toBeTruthy() + expect(appointmentStore.status).toEqual('loading') }) it('should handle the DELETE_APPOINTMENT_SUCCESS action', () => { @@ -114,7 +118,7 @@ describe('appointment slice', () => { type: deleteAppointmentSuccess.type, }) - expect(appointmentStore.isLoading).toBeFalsy() + expect(appointmentStore.status).toEqual('completed') }) }) @@ -182,6 +186,29 @@ describe('appointment slice', () => { expect(onSuccessSpy).toHaveBeenCalledWith(expectedSavedAppointment) }) + + it('should validate the appointment', async () => { + const dispatch = jest.fn() + const getState = jest.fn() + const expectedAppointment = { + startDateTime: new Date().toISOString(), + endDateTime: subDays(new Date(), 5).toISOString(), + location: 'location', + type: 'type', + reason: 'reason', + } as Appointment + + await createAppointment(expectedAppointment)(dispatch, getState, null) + + expect(dispatch).toHaveBeenNthCalledWith(2, { + type: createAppointmentError.type, + payload: { + message: 'scheduling.appointment.errors.createAppointmentError', + patient: 'scheduling.appointment.errors.patientRequired', + startDateTime: 'scheduling.appointment.errors.startDateMustBeBeforeEndDate', + }, + }) + }) }) describe('fetchAppointment()', () => { @@ -303,8 +330,16 @@ describe('appointment slice', () => { const dispatch = jest.fn() const getState = jest.fn() jest.spyOn(AppointmentRepository, 'saveOrUpdate') - const expectedAppointmentId = 'sliceId9' - const expectedAppointment = { id: expectedAppointmentId } as Appointment + + const expectedAppointment = { + patientId: 'sliceId9', + startDateTime: new Date().toISOString(), + endDateTime: new Date().toISOString(), + location: 'location', + type: 'type', + reason: 'reason', + } as Appointment + const mockedAppointmentRepository = mocked(AppointmentRepository, true) mockedAppointmentRepository.saveOrUpdate.mockResolvedValue(expectedAppointment) @@ -317,8 +352,16 @@ describe('appointment slice', () => { const dispatch = jest.fn() const getState = jest.fn() jest.spyOn(AppointmentRepository, 'saveOrUpdate') - const expectedAppointmentId = 'sliceId10' - const expectedAppointment = { id: expectedAppointmentId } as Appointment + + const expectedAppointment = { + patientId: 'sliceId10', + startDateTime: new Date().toISOString(), + endDateTime: new Date().toISOString(), + location: 'location', + type: 'type', + reason: 'reason', + } as Appointment + const mockedAppointmentRepository = mocked(AppointmentRepository, true) mockedAppointmentRepository.saveOrUpdate.mockResolvedValue(expectedAppointment) @@ -331,8 +374,16 @@ describe('appointment slice', () => { const dispatch = jest.fn() const getState = jest.fn() jest.spyOn(AppointmentRepository, 'saveOrUpdate') - const expectedAppointmentId = 'sliceId11' - const expectedAppointment = { id: expectedAppointmentId } as Appointment + + const expectedAppointment = { + patientId: 'sliceId11', + startDateTime: new Date().toISOString(), + endDateTime: new Date().toISOString(), + location: 'location', + type: 'type', + reason: 'reason', + } as Appointment + const mockedAppointmentRepository = mocked(AppointmentRepository, true) mockedAppointmentRepository.saveOrUpdate.mockResolvedValue(expectedAppointment) @@ -349,8 +400,16 @@ describe('appointment slice', () => { const dispatch = jest.fn() const getState = jest.fn() jest.spyOn(AppointmentRepository, 'saveOrUpdate') - const expectedAppointmentId = 'sliceId11' - const expectedAppointment = { id: expectedAppointmentId } as Appointment + + const expectedAppointment = { + patientId: 'sliceId12', + startDateTime: new Date().toISOString(), + endDateTime: new Date().toISOString(), + location: 'location', + type: 'type', + reason: 'reason', + } as Appointment + const mockedAppointmentRepository = mocked(AppointmentRepository, true) mockedAppointmentRepository.saveOrUpdate.mockResolvedValue(expectedAppointment) @@ -358,5 +417,28 @@ describe('appointment slice', () => { expect(onSuccessSpy).toHaveBeenCalledWith(expectedAppointment) }) + + it('should validate the appointment', async () => { + const dispatch = jest.fn() + const getState = jest.fn() + const expectedAppointment = { + startDateTime: new Date().toISOString(), + endDateTime: subDays(new Date(), 5).toISOString(), + location: 'location', + type: 'type', + reason: 'reason', + } as Appointment + + await updateAppointment(expectedAppointment)(dispatch, getState, null) + + expect(dispatch).toHaveBeenNthCalledWith(2, { + type: updateAppointmentError.type, + payload: { + message: 'scheduling.appointment.errors.updateAppointmentError', + patient: 'scheduling.appointment.errors.patientRequired', + startDateTime: 'scheduling.appointment.errors.startDateMustBeBeforeEndDate', + }, + }) + }) }) }) diff --git a/src/__tests__/scheduling/appointments/edit/EditAppointment.test.tsx b/src/__tests__/scheduling/appointments/edit/EditAppointment.test.tsx index 9cbad651ba..0a4cdca935 100644 --- a/src/__tests__/scheduling/appointments/edit/EditAppointment.test.tsx +++ b/src/__tests__/scheduling/appointments/edit/EditAppointment.test.tsx @@ -8,8 +8,8 @@ import { createMemoryHistory } from 'history' import { act } from 'react-dom/test-utils' import configureMockStore, { MockStore } from 'redux-mock-store' import thunk from 'redux-thunk' -import { roundToNearestMinutes, addMinutes, subDays } from 'date-fns' -import { Button, Alert } from '@hospitalrun/components' +import { roundToNearestMinutes, addMinutes } from 'date-fns' +import { Button } from '@hospitalrun/components' import EditAppointment from '../../../../scheduling/appointments/edit/EditAppointment' import AppointmentDetailForm from '../../../../scheduling/appointments/AppointmentDetailForm' import Appointment from '../../../../model/Appointment' @@ -119,48 +119,6 @@ describe('Edit Appointment', () => { expect(titleUtil.default).toHaveBeenCalledWith('scheduling.appointments.editAppointment') }) - it('should display an error if the end date is before the start date', async () => { - let wrapper: any - await act(async () => { - wrapper = await setup() - }) - - const startDateTime = roundToNearestMinutes(new Date(), { nearestTo: 15 }) - const endDateTime = subDays(startDateTime, 1) - - wrapper.update() - - act(() => { - const appointmentDetailForm = wrapper.find(AppointmentDetailForm) - const onFieldChange = appointmentDetailForm.prop('onFieldChange') - onFieldChange('startDateTime', startDateTime) - }) - - wrapper.update() - - act(() => { - const appointmentDetailForm = wrapper.find(AppointmentDetailForm) - const onFieldChange = appointmentDetailForm.prop('onFieldChange') - onFieldChange('endDateTime', endDateTime) - }) - - wrapper.update() - - act(() => { - const saveButton = wrapper.find(Button).at(0) - const onClick = saveButton.prop('onClick') as any - onClick() - }) - - wrapper.update() - - const alert = wrapper.find(Alert) - expect(alert).toHaveLength(1) - expect(alert.prop('message')).toEqual( - 'scheduling.appointment.errors.startDateMustBeBeforeEndDate', - ) - }) - it('should dispatch updateAppointment when save button is clicked', async () => { let wrapper: any await act(async () => { diff --git a/src/__tests__/scheduling/appointments/new/NewAppointment.test.tsx b/src/__tests__/scheduling/appointments/new/NewAppointment.test.tsx index 7d79ec1b67..492add87cc 100644 --- a/src/__tests__/scheduling/appointments/new/NewAppointment.test.tsx +++ b/src/__tests__/scheduling/appointments/new/NewAppointment.test.tsx @@ -7,7 +7,6 @@ import { mount } from 'enzyme' import { roundToNearestMinutes, addMinutes } from 'date-fns' import { createMemoryHistory, MemoryHistory } from 'history' import { act } from '@testing-library/react' -import subDays from 'date-fns/subDays' import AppointmentRepository from 'clients/db/AppointmentRepository' import { mocked } from 'ts-jest/utils' import configureMockStore, { MockStore } from 'redux-mock-store' @@ -201,73 +200,6 @@ describe('New Appointment', () => { `scheduling.appointment.successfullyCreated`, ) }) - - it('should display an error if there is no patient id', async () => { - let wrapper: any - await act(async () => { - wrapper = await setup() - }) - - act(() => { - const saveButton = wrapper.find(mockedComponents.Button).at(0) - const onClick = saveButton.prop('onClick') as any - onClick() - }) - wrapper.update() - - const alert = wrapper.find(mockedComponents.Alert) - expect(alert).toHaveLength(1) - expect(alert.prop('message')).toEqual('scheduling.appointment.errors.patientRequired') - }) - - it('should display an error if the end date is before the start date', async () => { - let wrapper: any - await act(async () => { - wrapper = await setup() - }) - - const patientId = '123' - const startDateTime = roundToNearestMinutes(new Date(), { nearestTo: 15 }) - const endDateTime = subDays(startDateTime, 1) - - act(() => { - const appointmentDetailForm = wrapper.find(AppointmentDetailForm) - const onFieldChange = appointmentDetailForm.prop('onFieldChange') - onFieldChange('patientId', patientId) - }) - - wrapper.update() - - act(() => { - const appointmentDetailForm = wrapper.find(AppointmentDetailForm) - const onFieldChange = appointmentDetailForm.prop('onFieldChange') - onFieldChange('startDateTime', startDateTime) - }) - - wrapper.update() - - act(() => { - const appointmentDetailForm = wrapper.find(AppointmentDetailForm) - const onFieldChange = appointmentDetailForm.prop('onFieldChange') - onFieldChange('endDateTime', endDateTime) - }) - - wrapper.update() - - act(() => { - const saveButton = wrapper.find(mockedComponents.Button).at(0) - const onClick = saveButton.prop('onClick') as any - onClick() - }) - - wrapper.update() - - const alert = wrapper.find(mockedComponents.Alert) - expect(alert).toHaveLength(1) - expect(alert.prop('message')).toEqual( - 'scheduling.appointment.errors.startDateMustBeBeforeEndDate', - ) - }) }) describe('on cancel click', () => { diff --git a/src/__tests__/scheduling/appointments/view/ViewAppointment.test.tsx b/src/__tests__/scheduling/appointments/view/ViewAppointment.test.tsx index d3b7f63abd..e090672c51 100644 --- a/src/__tests__/scheduling/appointments/view/ViewAppointment.test.tsx +++ b/src/__tests__/scheduling/appointments/view/ViewAppointment.test.tsx @@ -39,7 +39,7 @@ describe('View Appointment', () => { let history: any let store: MockStore - const setup = (isLoading: boolean, permissions = [Permissions.ReadAppointments]) => { + const setup = (status: string, permissions = [Permissions.ReadAppointments]) => { jest.spyOn(AppointmentRepository, 'find') jest.spyOn(AppointmentRepository, 'delete') const mockedAppointmentRepository = mocked(AppointmentRepository, true) @@ -59,7 +59,7 @@ describe('View Appointment', () => { }, appointment: { appointment, - isLoading, + status, patient, }, }) @@ -85,7 +85,7 @@ describe('View Appointment', () => { it('should use the correct title', async () => { jest.spyOn(titleUtil, 'default') await act(async () => { - await setup(true) + await setup('loading') }) expect(titleUtil.default).toHaveBeenCalledWith('scheduling.appointments.viewAppointment') @@ -96,7 +96,7 @@ describe('View Appointment', () => { const setButtonToolBarSpy = jest.fn() mocked(ButtonBarProvider).useButtonToolbarSetter.mockReturnValue(setButtonToolBarSpy) - setup(true, [Permissions.WriteAppointments, Permissions.ReadAppointments]) + setup('loading', [Permissions.WriteAppointments, Permissions.ReadAppointments]) const actualButtons: React.ReactNode[] = setButtonToolBarSpy.mock.calls[0][0] expect((actualButtons[0] as any).props.children).toEqual('actions.edit') @@ -107,7 +107,7 @@ describe('View Appointment', () => { const setButtonToolBarSpy = jest.fn() mocked(ButtonBarProvider).useButtonToolbarSetter.mockReturnValue(setButtonToolBarSpy) - setup(true, [Permissions.DeleteAppointment, Permissions.ReadAppointments]) + setup('loading', [Permissions.DeleteAppointment, Permissions.ReadAppointments]) const actualButtons: React.ReactNode[] = setButtonToolBarSpy.mock.calls[0][0] expect((actualButtons[0] as any).props.children).toEqual( @@ -120,7 +120,7 @@ describe('View Appointment', () => { const setButtonToolBarSpy = jest.fn() mocked(ButtonBarProvider).useButtonToolbarSetter.mockReturnValue(setButtonToolBarSpy) - setup(true) + setup('loading') const actualButtons: React.ReactNode[] = setButtonToolBarSpy.mock.calls[0][0] expect(actualButtons.length).toEqual(0) @@ -128,7 +128,7 @@ describe('View Appointment', () => { it('should dispatch getAppointment if id is present', async () => { await act(async () => { - await setup(true) + await setup('loading') }) expect(AppointmentRepository.find).toHaveBeenCalledWith(appointment.id) @@ -141,7 +141,7 @@ describe('View Appointment', () => { it('should render a loading spinner', async () => { let wrapper: any await act(async () => { - wrapper = await setup(true) + wrapper = await setup('loading') }) expect(wrapper.find(components.Spinner)).toHaveLength(1) @@ -150,7 +150,7 @@ describe('View Appointment', () => { it('should render a AppointmentDetailForm with the correct data', async () => { let wrapper: any await act(async () => { - wrapper = await setup(false) + wrapper = await setup('completed') }) const appointmentDetailForm = wrapper.find(AppointmentDetailForm) @@ -161,7 +161,7 @@ describe('View Appointment', () => { it('should render a modal for delete confirmation', async () => { let wrapper: any await act(async () => { - wrapper = await setup(false) + wrapper = await setup('completed') }) const deleteAppointmentConfirmationModal = wrapper.find(components.Modal) @@ -188,7 +188,7 @@ describe('View Appointment', () => { it('should render a delete appointment button in the button toolbar', async () => { await act(async () => { - await setup(false, [Permissions.ReadAppointments, Permissions.DeleteAppointment]) + await setup('completed', [Permissions.ReadAppointments, Permissions.DeleteAppointment]) }) expect(setButtonToolBarSpy).toHaveBeenCalledTimes(1) @@ -201,7 +201,10 @@ describe('View Appointment', () => { it('should pop up the modal when on delete appointment click', async () => { let wrapper: any await act(async () => { - wrapper = await setup(false, [Permissions.ReadAppointments, Permissions.DeleteAppointment]) + wrapper = await setup('completed', [ + Permissions.ReadAppointments, + Permissions.DeleteAppointment, + ]) }) expect(setButtonToolBarSpy).toHaveBeenCalledTimes(1) @@ -220,7 +223,10 @@ describe('View Appointment', () => { it('should close the modal when the toggle button is clicked', async () => { let wrapper: any await act(async () => { - wrapper = await setup(false, [Permissions.ReadAppointments, Permissions.DeleteAppointment]) + wrapper = await setup('completed', [ + Permissions.ReadAppointments, + Permissions.DeleteAppointment, + ]) }) expect(setButtonToolBarSpy).toHaveBeenCalledTimes(1) @@ -245,7 +251,10 @@ describe('View Appointment', () => { it('should dispatch DELETE_APPOINTMENT action when modal confirmation button is clicked', async () => { let wrapper: any await act(async () => { - wrapper = await setup(false, [Permissions.ReadAppointments, Permissions.DeleteAppointment]) + wrapper = await setup('completed', [ + Permissions.ReadAppointments, + Permissions.DeleteAppointment, + ]) }) const deleteConfirmationModal = wrapper.find(components.Modal) @@ -268,7 +277,10 @@ describe('View Appointment', () => { let wrapper: any await act(async () => { - wrapper = await setup(false, [Permissions.ReadAppointments, Permissions.DeleteAppointment]) + wrapper = await setup('completed', [ + Permissions.ReadAppointments, + Permissions.DeleteAppointment, + ]) }) const deleteConfirmationModal = wrapper.find(components.Modal) diff --git a/src/components/input/DateTimePickerWithLabelFormGroup.tsx b/src/components/input/DateTimePickerWithLabelFormGroup.tsx index cd38d51dac..f31a186692 100644 --- a/src/components/input/DateTimePickerWithLabelFormGroup.tsx +++ b/src/components/input/DateTimePickerWithLabelFormGroup.tsx @@ -7,10 +7,12 @@ interface Props { value: Date | undefined isEditable?: boolean onChange?: (date: Date) => void + isInvalid?: boolean + feedback?: string } const DateTimePickerWithLabelFormGroup = (props: Props) => { - const { onChange, label, name, isEditable, value } = props + const { onChange, label, name, isEditable, value, isInvalid, feedback } = props const id = `${name}DateTimePicker` return (
@@ -21,6 +23,8 @@ const DateTimePickerWithLabelFormGroup = (props: Props) => { dropdownMode="scroll" disabled={!isEditable} selected={value} + isInvalid={isInvalid} + feedback={feedback} onChange={(inputDate) => { if (onChange) { onChange(inputDate) diff --git a/src/labs/lab-slice.ts b/src/labs/lab-slice.ts index e4e8b880d5..224ffbd722 100644 --- a/src/labs/lab-slice.ts +++ b/src/labs/lab-slice.ts @@ -16,7 +16,7 @@ interface LabState { error: Error lab?: Lab patient?: Patient - status: 'loading' | 'error' | 'success' + status: 'loading' | 'error' | 'completed' } const initialState: LabState = { @@ -31,7 +31,7 @@ function start(state: LabState) { } function finish(state: LabState, { payload }: PayloadAction) { - state.status = 'success' + state.status = 'completed' state.lab = payload state.error = {} } @@ -50,7 +50,7 @@ const labSlice = createSlice({ state: LabState, { payload }: PayloadAction<{ lab: Lab; patient: Patient }>, ) => { - state.status = 'success' + state.status = 'completed' state.lab = payload.lab state.patient = payload.patient }, diff --git a/src/locales/enUs/translations/scheduling/index.ts b/src/locales/enUs/translations/scheduling/index.ts index 29ef257ede..1ac6f8bf46 100644 --- a/src/locales/enUs/translations/scheduling/index.ts +++ b/src/locales/enUs/translations/scheduling/index.ts @@ -22,14 +22,16 @@ export default { walkIn: 'Walk In', }, errors: { + createAppointmentError: 'Could not create new appointment.', + updateAppointmentError: 'Could not update appointment.', patientRequired: 'Patient is required.', - errorCreatingAppointment: 'Error Creating Appointment!', startDateMustBeBeforeEndDate: 'Start Time must be before End Time.', }, reason: 'Reason', patient: 'Patient', deleteConfirmationMessage: 'Are you sure that you want to delete this appointment?', successfullyCreated: 'Successfully created appointment.', + successfullyDeleted: 'Successfully deleted appointment.', }, }, } diff --git a/src/locales/fr/translations/scheduling/index.ts b/src/locales/fr/translations/scheduling/index.ts index 060309087d..a0ef8c6156 100644 --- a/src/locales/fr/translations/scheduling/index.ts +++ b/src/locales/fr/translations/scheduling/index.ts @@ -22,7 +22,6 @@ export default { }, errors: { patientRequired: 'Le patient est requis.', - errorCreatingAppointment: 'Erreur lors de la création du rendez-vous !', startDateMustBeBeforeEndDate: "La date de début doit être inférieur à l'heure de fin.", }, reason: 'Raison', diff --git a/src/locales/ptBr/translations/scheduling/index.ts b/src/locales/ptBr/translations/scheduling/index.ts index 206c489124..4882c384ae 100644 --- a/src/locales/ptBr/translations/scheduling/index.ts +++ b/src/locales/ptBr/translations/scheduling/index.ts @@ -23,7 +23,6 @@ export default { }, errors: { patientRequired: 'Paciente é necessario.', - errorCreatingAppointment: 'Algo deu errado criando o agendamento.', startDateMustBeBeforeEndDate: 'Horário de início deve ser antes do horário final.', }, reason: 'Razão', diff --git a/src/patients/patient-slice.ts b/src/patients/patient-slice.ts index ba79c89dc9..f106abb18e 100644 --- a/src/patients/patient-slice.ts +++ b/src/patients/patient-slice.ts @@ -71,7 +71,7 @@ const initialState: PatientState = { relatedPersonError: undefined, } -function startLoading(state: PatientState) { +function start(state: PatientState) { state.status = 'loading' state.createError = {} } @@ -80,12 +80,12 @@ const patientSlice = createSlice({ name: 'patient', initialState, reducers: { - fetchPatientStart: startLoading, + fetchPatientStart: start, fetchPatientSuccess(state, { payload }: PayloadAction) { state.status = 'completed' state.patient = payload }, - createPatientStart: startLoading, + createPatientStart: start, createPatientSuccess(state) { state.status = 'completed' }, @@ -93,7 +93,7 @@ const patientSlice = createSlice({ state.status = 'error' state.createError = payload }, - updatePatientStart: startLoading, + updatePatientStart: start, updatePatientSuccess(state, { payload }: PayloadAction) { state.status = 'completed' state.patient = payload diff --git a/src/scheduling/appointments/AppointmentDetailForm.tsx b/src/scheduling/appointments/AppointmentDetailForm.tsx index 155ee956ec..6fe9837a3f 100644 --- a/src/scheduling/appointments/AppointmentDetailForm.tsx +++ b/src/scheduling/appointments/AppointmentDetailForm.tsx @@ -13,12 +13,12 @@ interface Props { appointment: Appointment patient?: Patient isEditable: boolean - errorMessage?: string + error?: any onFieldChange?: (key: string, value: string | boolean) => void } const AppointmentDetailForm = (props: Props) => { - const { onFieldChange, appointment, patient, isEditable, errorMessage } = props + const { onFieldChange, appointment, patient, isEditable, error } = props const { t } = useTranslation() const onSelectChange = (event: React.ChangeEvent, fieldName: string) => @@ -32,7 +32,7 @@ const AppointmentDetailForm = (props: Props) => { return ( <> - {errorMessage && } + {error?.message && }
@@ -46,10 +46,15 @@ const AppointmentDetailForm = (props: Props) => { disabled={!isEditable || patient !== undefined} value={patient?.fullName} placeholder={t('scheduling.appointment.patient')} - onChange={(p: Patient[]) => onFieldChange && onFieldChange('patientId', p[0].id)} + onChange={ + (p: Patient[]) => onFieldChange && p[0] && onFieldChange('patientId', p[0].id) + // eslint-disable-next-line react/jsx-curly-newline + } onSearch={async (query: string) => PatientRepository.search(query)} searchAccessor="fullName" renderMenuItemChildren={(p: Patient) =>
{`${p.fullName} (${p.code})`}
} + isInvalid={!!error?.patient} + feedback={t(error?.patient)} />
@@ -59,8 +64,14 @@ const AppointmentDetailForm = (props: Props) => { 0 + ? new Date(appointment.startDateTime) + : undefined + } isEditable={isEditable} + isInvalid={error?.startDateTime} + feedback={t(error?.startDateTime)} onChange={(date: Date) => { onDateChange(date, 'startDateTime') }} @@ -70,7 +81,11 @@ const AppointmentDetailForm = (props: Props) => { 0 + ? new Date(appointment.endDateTime) + : undefined + } isEditable={isEditable} onChange={(date: Date) => { onDateChange(date, 'endDateTime') diff --git a/src/scheduling/appointments/Appointments.tsx b/src/scheduling/appointments/Appointments.tsx index 74cbcb4143..142bff601a 100644 --- a/src/scheduling/appointments/Appointments.tsx +++ b/src/scheduling/appointments/Appointments.tsx @@ -1,86 +1,46 @@ -import React, { useEffect, useState } from 'react' -import { Calendar, Button } from '@hospitalrun/components' -import useTitle from 'page-header/useTitle' -import { useTranslation } from 'react-i18next' -import { useSelector, useDispatch } from 'react-redux' -import { RootState } from 'store' -import { useHistory } from 'react-router' -import PatientRepository from 'clients/db/PatientRepository' -import useAddBreadcrumbs from 'breadcrumbs/useAddBreadcrumbs' -import { useButtonToolbarSetter } from 'page-header/ButtonBarProvider' -import { fetchAppointments } from './appointments-slice' - -interface Event { - id: string - start: Date - end: Date - title: string - allDay: boolean -} - -const breadcrumbs = [{ i18nKey: 'scheduling.appointments.label', location: '/appointments' }] +import React from 'react' +import { useSelector } from 'react-redux' +import { Switch } from 'react-router' +import NewAppointment from 'scheduling/appointments/new/NewAppointment' +import EditAppointment from 'scheduling/appointments/edit/EditAppointment' +import ViewAppointment from 'scheduling/appointments/view/ViewAppointment' +import ViewAppointments from './ViewAppointments' +import PrivateRoute from '../../components/PrivateRoute' +import Permissions from '../../model/Permissions' +import { RootState } from '../../store' const Appointments = () => { - const { t } = useTranslation() - const history = useHistory() - useTitle(t('scheduling.appointments.label')) - const dispatch = useDispatch() - const { appointments } = useSelector((state: RootState) => state.appointments) - const [events, setEvents] = useState([]) - const setButtonToolBar = useButtonToolbarSetter() - useAddBreadcrumbs(breadcrumbs, true) - - useEffect(() => { - dispatch(fetchAppointments()) - setButtonToolBar([ - , - ]) - - return () => { - setButtonToolBar([]) - } - }, [dispatch, setButtonToolBar, history, t]) - - useEffect(() => { - const getAppointments = async () => { - const newEvents = await Promise.all( - appointments.map(async (a) => { - const patient = await PatientRepository.find(a.patientId) - return { - id: a.id, - start: new Date(a.startDateTime), - end: new Date(a.endDateTime), - title: patient.fullName || '', - allDay: false, - } - }), - ) - - setEvents(newEvents) - } - - if (appointments) { - getAppointments() - } - }, [appointments]) - + const permissions = useSelector((state: RootState) => state.user.permissions) return ( -
- { - history.push(`/appointments/${event.id}`) - }} + + + + + -
+ ) } diff --git a/src/scheduling/appointments/ViewAppointments.tsx b/src/scheduling/appointments/ViewAppointments.tsx new file mode 100644 index 0000000000..b569c2b4a7 --- /dev/null +++ b/src/scheduling/appointments/ViewAppointments.tsx @@ -0,0 +1,87 @@ +import React, { useEffect, useState } from 'react' +import { Calendar, Button } from '@hospitalrun/components' +import useTitle from 'page-header/useTitle' +import { useTranslation } from 'react-i18next' +import { useSelector, useDispatch } from 'react-redux' +import { RootState } from 'store' +import { useHistory } from 'react-router' +import PatientRepository from 'clients/db/PatientRepository' +import useAddBreadcrumbs from 'breadcrumbs/useAddBreadcrumbs' +import { useButtonToolbarSetter } from 'page-header/ButtonBarProvider' +import { fetchAppointments } from './appointments-slice' + +interface Event { + id: string + start: Date + end: Date + title: string + allDay: boolean +} + +const breadcrumbs = [{ i18nKey: 'scheduling.appointments.label', location: '/appointments' }] + +const ViewAppointments = () => { + const { t } = useTranslation() + const history = useHistory() + useTitle(t('scheduling.appointments.label')) + const dispatch = useDispatch() + const { appointments } = useSelector((state: RootState) => state.appointments) + const [events, setEvents] = useState([]) + const setButtonToolBar = useButtonToolbarSetter() + useAddBreadcrumbs(breadcrumbs, true) + + useEffect(() => { + dispatch(fetchAppointments()) + setButtonToolBar([ + , + ]) + + return () => { + setButtonToolBar([]) + } + }, [dispatch, setButtonToolBar, history, t]) + + useEffect(() => { + const getAppointments = async () => { + const newEvents = await Promise.all( + appointments.map(async (a) => { + const patient = await PatientRepository.find(a.patientId) + return { + id: a.id, + start: new Date(a.startDateTime), + end: new Date(a.endDateTime), + title: patient.fullName || '', + allDay: false, + } + }), + ) + + setEvents(newEvents) + } + + if (appointments) { + getAppointments() + } + }, [appointments]) + + return ( +
+ { + history.push(`/appointments/${event.id}`) + }} + /> +
+ ) +} + +export default ViewAppointments diff --git a/src/scheduling/appointments/appointment-slice.ts b/src/scheduling/appointments/appointment-slice.ts index 04d0bd6ea4..af8732531d 100644 --- a/src/scheduling/appointments/appointment-slice.ts +++ b/src/scheduling/appointments/appointment-slice.ts @@ -4,48 +4,82 @@ import { AppThunk } from 'store' import AppointmentRepository from 'clients/db/AppointmentRepository' import Patient from 'model/Patient' import PatientRepository from 'clients/db/PatientRepository' +import { isBefore } from 'date-fns' +import _ from 'lodash' + +function validateAppointment(appointment: Appointment) { + const err: Error = {} + + if (!appointment.patientId) { + err.patient = 'scheduling.appointment.errors.patientRequired' + } + + if (isBefore(new Date(appointment.endDateTime), new Date(appointment.startDateTime))) { + err.startDateTime = 'scheduling.appointment.errors.startDateMustBeBeforeEndDate' + } + + return err +} + +interface Error { + patient?: string + startDateTime?: string + message?: string +} interface AppointmentState { + error: Error appointment: Appointment patient: Patient - isLoading: boolean + status: 'loading' | 'error' | 'completed' } -const initialAppointmentState = { +const initialState: AppointmentState = { + error: {}, appointment: {} as Appointment, patient: {} as Patient, - isLoading: false, + status: 'loading', } -function startLoading(state: AppointmentState) { - state.isLoading = true +function start(state: AppointmentState) { + state.status = 'loading' +} + +function finish(state: AppointmentState, { payload }: PayloadAction) { + state.status = 'completed' + state.appointment = payload + state.error = {} +} + +function error(state: AppointmentState, { payload }: PayloadAction) { + state.status = 'error' + state.error = payload } const appointmentSlice = createSlice({ name: 'appointment', - initialState: initialAppointmentState, + initialState, reducers: { - fetchAppointmentStart: startLoading, - createAppointmentStart: startLoading, - updateAppointmentStart: startLoading, - deleteAppointmentStart: startLoading, - deleteAppointmentSuccess: (state: AppointmentState) => { - state.isLoading = false - }, + fetchAppointmentStart: start, fetchAppointmentSuccess: ( - state, + state: AppointmentState, { payload }: PayloadAction<{ appointment: Appointment; patient: Patient }>, ) => { - state.isLoading = false + state.status = 'completed' state.appointment = payload.appointment state.patient = payload.patient }, + createAppointmentStart: start, createAppointmentSuccess(state) { - state.isLoading = false + state.status = 'completed' }, - updateAppointmentSuccess(state, { payload }: PayloadAction) { - state.isLoading = false - state.appointment = payload + createAppointmentError: error, + updateAppointmentStart: start, + updateAppointmentSuccess: finish, + updateAppointmentError: error, + deleteAppointmentStart: start, + deleteAppointmentSuccess(state) { + state.status = 'completed' }, }, }) @@ -53,8 +87,10 @@ const appointmentSlice = createSlice({ export const { createAppointmentStart, createAppointmentSuccess, + createAppointmentError, updateAppointmentStart, updateAppointmentSuccess, + updateAppointmentError, fetchAppointmentStart, fetchAppointmentSuccess, deleteAppointmentStart, @@ -74,10 +110,17 @@ export const createAppointment = ( onSuccess?: (appointment: Appointment) => void, ): AppThunk => async (dispatch) => { dispatch(createAppointmentStart()) - const newAppointment = await AppointmentRepository.save(appointment) - dispatch(createAppointmentSuccess()) - if (onSuccess) { - onSuccess(newAppointment) + const newAppointmentError = validateAppointment(appointment) + + if (_.isEmpty(newAppointmentError)) { + const newAppointment = await AppointmentRepository.save(appointment) + dispatch(createAppointmentSuccess()) + if (onSuccess) { + onSuccess(newAppointment) + } + } else { + newAppointmentError.message = 'scheduling.appointment.errors.createAppointmentError' + dispatch(createAppointmentError(newAppointmentError)) } } @@ -86,11 +129,18 @@ export const updateAppointment = ( onSuccess?: (appointment: Appointment) => void, ): AppThunk => async (dispatch) => { dispatch(updateAppointmentStart()) - const updatedAppointment = await AppointmentRepository.saveOrUpdate(appointment) - dispatch(updateAppointmentSuccess(updatedAppointment)) + const updatedAppointmentError = validateAppointment(appointment) - if (onSuccess) { - onSuccess(updatedAppointment) + if (_.isEmpty(updatedAppointmentError)) { + const updatedAppointment = await AppointmentRepository.saveOrUpdate(appointment) + dispatch(updateAppointmentSuccess(updatedAppointment)) + + if (onSuccess) { + onSuccess(updatedAppointment) + } + } else { + updatedAppointmentError.message = 'scheduling.appointment.errors.updateAppointmentError' + dispatch(updateAppointmentError(updatedAppointmentError)) } } diff --git a/src/scheduling/appointments/edit/EditAppointment.tsx b/src/scheduling/appointments/edit/EditAppointment.tsx index e2d676b1ad..d52d757c46 100644 --- a/src/scheduling/appointments/edit/EditAppointment.tsx +++ b/src/scheduling/appointments/edit/EditAppointment.tsx @@ -3,7 +3,6 @@ import { useHistory, useParams } from 'react-router-dom' import { useTranslation } from 'react-i18next' import { useDispatch, useSelector } from 'react-redux' import { Spinner, Button } from '@hospitalrun/components' -import { isBefore } from 'date-fns' import AppointmentDetailForm from '../AppointmentDetailForm' import useTitle from '../../../page-header/useTitle' @@ -20,8 +19,8 @@ const EditAppointment = () => { const dispatch = useDispatch() const [appointment, setAppointment] = useState({} as Appointment) - const [errorMessage, setErrorMessage] = useState('') - const { appointment: reduxAppointment, patient, isLoading } = useSelector( + + const { appointment: reduxAppointment, patient, status, error } = useSelector( (state: RootState) => state.appointment, ) const breadcrumbs = [ @@ -57,16 +56,6 @@ const EditAppointment = () => { } const onSave = () => { - let newErrorMessage = '' - if (isBefore(new Date(appointment.endDateTime), new Date(appointment.startDateTime))) { - newErrorMessage += ` ${t('scheduling.appointment.errors.startDateMustBeBeforeEndDate')}` - } - - if (newErrorMessage) { - setErrorMessage(newErrorMessage.trim()) - return - } - dispatch(updateAppointment(appointment as Appointment, onSaveSuccess)) } @@ -77,7 +66,7 @@ const EditAppointment = () => { }) } - if (isLoading || !appointment.id || !patient.id) { + if (status === 'loading') { return } @@ -88,7 +77,7 @@ const EditAppointment = () => { appointment={appointment} patient={patient} onFieldChange={onFieldChange} - errorMessage={errorMessage} + error={error} />
diff --git a/src/scheduling/appointments/new/NewAppointment.tsx b/src/scheduling/appointments/new/NewAppointment.tsx index 69f14a7307..438869dc9e 100644 --- a/src/scheduling/appointments/new/NewAppointment.tsx +++ b/src/scheduling/appointments/new/NewAppointment.tsx @@ -3,14 +3,14 @@ import useTitle from 'page-header/useTitle' import { useTranslation } from 'react-i18next' import roundToNearestMinutes from 'date-fns/roundToNearestMinutes' import { useHistory } from 'react-router' -import { useDispatch } from 'react-redux' +import { useDispatch, useSelector } from 'react-redux' import Appointment from 'model/Appointment' import addMinutes from 'date-fns/addMinutes' -import { isBefore } from 'date-fns' import { Button, Toast } from '@hospitalrun/components' import useAddBreadcrumbs from '../../../breadcrumbs/useAddBreadcrumbs' import { createAppointment } from '../appointment-slice' import AppointmentDetailForm from '../AppointmentDetailForm' +import { RootState } from '../../../store' const breadcrumbs = [ { i18nKey: 'scheduling.appointments.label', location: '/appointments' }, @@ -25,6 +25,7 @@ const NewAppointment = () => { useAddBreadcrumbs(breadcrumbs, true) const startDateTime = roundToNearestMinutes(new Date(), { nearestTo: 15 }) const endDateTime = addMinutes(startDateTime, 60) + const { error } = useSelector((state: RootState) => state.appointment) const [appointment, setAppointment] = useState({ patientId: '', @@ -34,32 +35,18 @@ const NewAppointment = () => { reason: '', type: '', }) - const [errorMessage, setErrorMessage] = useState('') const onCancelClick = () => { history.push('/appointments') } - const onNewAppointmentSaveSuccess = (newAppointment: Appointment) => { + const onSaveSuccess = (newAppointment: Appointment) => { history.push(`/appointments/${newAppointment.id}`) Toast('success', t('states.success'), `${t('scheduling.appointment.successfullyCreated')}`) } const onSave = () => { - let newErrorMessage = '' - if (!appointment.patientId) { - newErrorMessage += t('scheduling.appointment.errors.patientRequired') - } - if (isBefore(new Date(appointment.endDateTime), new Date(appointment.startDateTime))) { - newErrorMessage += ` ${t('scheduling.appointment.errors.startDateMustBeBeforeEndDate')}` - } - - if (newErrorMessage) { - setErrorMessage(newErrorMessage.trim()) - return - } - - dispatch(createAppointment(appointment as Appointment, onNewAppointmentSaveSuccess)) + dispatch(createAppointment(appointment as Appointment, onSaveSuccess)) } const onFieldChange = (key: string, value: string | boolean) => { @@ -74,7 +61,7 @@ const NewAppointment = () => {
diff --git a/src/scheduling/appointments/view/ViewAppointment.tsx b/src/scheduling/appointments/view/ViewAppointment.tsx index f54bd98093..c90002b2e4 100644 --- a/src/scheduling/appointments/view/ViewAppointment.tsx +++ b/src/scheduling/appointments/view/ViewAppointment.tsx @@ -18,7 +18,7 @@ const ViewAppointment = () => { const dispatch = useDispatch() const { id } = useParams() const history = useHistory() - const { appointment, patient, isLoading } = useSelector((state: RootState) => state.appointment) + const { appointment, patient, status } = useSelector((state: RootState) => state.appointment) const { permissions } = useSelector((state: RootState) => state.user) const [showDeleteConfirmation, setShowDeleteConfirmation] = useState(false) const setButtonToolBar = useButtonToolbarSetter() @@ -88,7 +88,7 @@ const ViewAppointment = () => { } }, [dispatch, id, setButtonToolBar]) - if (!appointment.id || isLoading) { + if (status === 'loading') { return }