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 = () => {