diff --git a/package.json b/package.json index 9c126af..9a4f4fe 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,7 @@ "@emotion/jest": "^11.2.1", "@emotion/react": "^11.4.1", "@emotion/styled": "^11.3.0", - "@orfium/ictinus": "^3.9.1", + "@orfium/ictinus": "^3.14.1", "@testing-library/jest-dom": "^5.11.9", "@testing-library/react": "^12.1.2", "@testing-library/user-event": "^13.1.3", @@ -23,11 +23,13 @@ "axios": "^0.21.1", "customize-cra": "^1.0.0", "final-form": "^4.20.2", + "final-form-arrays": "^3.0.2", "history": "^4.10.1", "lodash": "^4.17.21", "react": "^17.0.2", "react-dom": "^17.0.2", "react-final-form": "^6.5.3", + "react-final-form-arrays": "^3.1.3", "react-final-form-listeners": "^1.0.3", "react-query": "^3.16.0", "react-router-dom": "^5.2.0", diff --git a/src/api/patientsAPI/patientsAPI.tsx b/src/api/patientsAPI/patientsAPI.tsx index 76f39ba..cfcecdd 100644 --- a/src/api/patientsAPI/patientsAPI.tsx +++ b/src/api/patientsAPI/patientsAPI.tsx @@ -1,12 +1,21 @@ /* eslint-disable @typescript-eslint/explicit-module-boundary-types */ -import { RegisterPatientPayload, PatientsPayload, PaginationParams } from '../../models/apiTypes'; +import { + RegisterPatientPayload, + PatientsPayload, + PaginationParams, + RegisterEpisodePayload, +} from '../../models/apiTypes'; import { METHODS, request } from '../axiosInstances'; export default { getHospitals: (params?: PaginationParams) => request(METHODS.GET, '/hospitals/', { params }), - getHospital: (id: string) => request(METHODS.GET, `/hospitals/${id}`, {}), + getHospital: (id: string) => request(METHODS.GET, `/hospitals/${id}/`, {}), getPatients: (params?: PatientsPayload) => request(METHODS.GET, '/patients/', { params }), - getPatient: (id: string) => request(METHODS.GET, `/patients/${id}`, {}), + getSurgeons: (params?: PaginationParams) => + request(METHODS.GET, '/medical-personnel/', { params }), + getPatient: (id: string) => request(METHODS.GET, `/patients/${id}/`, {}), registerPatient: (params: RegisterPatientPayload) => request(METHODS.POST, '/patients/', { params }), + registerEpisode: (params: RegisterEpisodePayload) => + request(METHODS.POST, '/episodes/', { params }), }; diff --git a/src/common.style.tsx b/src/common.style.tsx index 10f6123..1247eff 100644 --- a/src/common.style.tsx +++ b/src/common.style.tsx @@ -76,6 +76,15 @@ export const PageTitle = styled.div` padding: 16px; `; +export const PageSubtitle = styled.div` + color: ${(props) => props.theme.utils.getColor('darkGray', 400)}; + display: flex; + font-size: 18px; + font-weight: 400; + gap: 16px; + padding: 16px; +`; + export const SectionTitle = styled.div` color: ${(props) => props.theme.utils.getColor('blue', 500)}; font-size: 18px; diff --git a/src/hooks/api/patientHooks.ts b/src/hooks/api/patientHooks.ts index 8fc4a0b..2b947d1 100644 --- a/src/hooks/api/patientHooks.ts +++ b/src/hooks/api/patientHooks.ts @@ -11,8 +11,11 @@ import { PatientAPI, PatientsPayload, PatientsResponse, + RegisterEpisodePayload, RegisterPatientPayload, + SurgeonsResponse, } from '../../models/apiTypes'; +import { RegisterEpisodeFormType } from '../../pages/RegisterEpisode/types'; import { RegisterPatientFormType } from '../../pages/RegisterPatient/types'; import urls from '../../routing/urls'; @@ -77,6 +80,22 @@ export const useGetPatients = (params?: PatientsPayload) => { ); }; +export const useGetSurgeons = (params?: PaginationParams) => { + return useQuery( + [ReactQueryKeys.SurgeonsQuery, params?.limit, params?.offset, params?.ordering], + async () => { + const { request } = patientsAPI.single.getSurgeons(params); + return await request(); + }, + { + onError: (errors) => { + console.log(errors); + }, + retry: false, + } + ); +}; + export const useGetPatient = (id: string) => { return useQuery( [ReactQueryKeys.PatientsQuery, id], @@ -122,3 +141,44 @@ export const useRegisterPatient = () => { } ); }; + +export const useRegisterEpisode = ( + hospitalID?: string, + patientID?: string, + episodeType = 'Inguinal Mesh Hernia Repair' +) => { + const history = useHistory(); + + return useMutation( + (params) => { + const payload = { + hospital_id: params?.hospital?.value, + patient_id: parseInt(patientID ?? '0'), + anaesthetic_type: params?.anaestheticType?.label, + diathermy_used: params?.diathermyUsed?.label === 'True', + surgeon_ids: params?.surgeons?.map((surgeon) => surgeon?.value) ?? ['1'], + comments: params?.comments, + mesh_type: params?.meshType?.label, + episode_type: episodeType, + type: params.type?.label, + cepod: params.cepod?.label, + complexity: params?.complexity?.label, + occurence: params?.occurence?.label, + side: params?.side?.label, + surgery_date: params?.surgeryDate, + }; + + const { request } = patientsAPI.single.registerEpisode(payload); + + return request(); + }, + { + onSuccess: () => { + history.replace(`${urls.patients()}/${hospitalID}/${patientID}`); + }, + onError: (errors) => { + console.log(errors); + }, + } + ); +}; diff --git a/src/hooks/constants.ts b/src/hooks/constants.ts index 34cab88..f9ae9ac 100644 --- a/src/hooks/constants.ts +++ b/src/hooks/constants.ts @@ -1,4 +1,5 @@ export const ReactQueryKeys = { PatientsQuery: 'patientsQuery', HospitalsQuery: 'hospitalsQuery', + SurgeonsQuery: 'surgeonsQuery', }; diff --git a/src/models/apiTypes.tsx b/src/models/apiTypes.tsx index a4a568b..2cac2e1 100644 --- a/src/models/apiTypes.tsx +++ b/src/models/apiTypes.tsx @@ -36,7 +36,36 @@ export type HospitalsAPI = { patient_hospital_id?: number; }; -// export type EpisodesAPI = {}; +export type EpisodesAPI = { + episode_type: string; + cepod: string; + side: string; + occurence: string; + type: string; + complexity: string; + mesh_type: string; + diathermy_used: boolean; + comments?: string; + anaesthetic_type: string; + surgeons: SurgeonsAPI[]; +}; + +export type RegisterEpisodePayload = { + hospital_id: number; + patient_id: number; + surgery_date: string; + episode_type: string; + cepod: string; + side: string; + occurence: string; + type: string; + complexity: string; + mesh_type: string; + diathermy_used: boolean; + comments?: string; + anaesthetic_type: string; + surgeon_ids: number[]; +}; export interface HospitalsResponse extends PaginationResponse, PaginationParams { results: HospitalsAPI[]; @@ -73,9 +102,20 @@ export type PatientAPI = { phone_2: string; address: string; hospital_mappings: HospitalsAPI[]; - // episodes: EpisodesAPI[]; + episodes: EpisodesAPI[]; }; export interface PatientsResponse extends PaginationResponse, PaginationParams { results: PatientAPI[]; } + +export type SurgeonsAPI = { + id: number; + user: { + email: string; + }; + level: string; +}; +export interface SurgeonsResponse extends PaginationResponse, PaginationParams { + results: SurgeonsAPI[]; +} diff --git a/src/pages/PatientDetails/PatientDetails.tsx b/src/pages/PatientDetails/PatientDetails.tsx index 6643d5c..b7acbe8 100644 --- a/src/pages/PatientDetails/PatientDetails.tsx +++ b/src/pages/PatientDetails/PatientDetails.tsx @@ -57,7 +57,14 @@ const PatientDetails: React.FC = () => { )} - diff --git a/src/pages/RegisterEpisode/RegisterEpisode.style.ts b/src/pages/RegisterEpisode/RegisterEpisode.style.ts new file mode 100644 index 0000000..923ea31 --- /dev/null +++ b/src/pages/RegisterEpisode/RegisterEpisode.style.ts @@ -0,0 +1,8 @@ +import styled from '@emotion/styled'; + +export const FormHeading = styled.span` + font-size: 14px; + font-weight: 700; + margin-bottom: 12px; + padding: 18px; +`; diff --git a/src/pages/RegisterEpisode/RegisterEpisode.tsx b/src/pages/RegisterEpisode/RegisterEpisode.tsx new file mode 100644 index 0000000..ae11f1d --- /dev/null +++ b/src/pages/RegisterEpisode/RegisterEpisode.tsx @@ -0,0 +1,149 @@ +/** @jsxImportSource @emotion/react */ +import React, { useState } from 'react'; + +import { Button, Icon } from '@orfium/ictinus'; +import { ButtonContainer, PageSubtitle, PageTitle, PageWrapper } from 'common.style'; +import ConfirmationModal from 'components/ConfirmationModal'; +import arrayMutators from 'final-form-arrays'; +import { Form } from 'react-final-form'; +import { useHistory } from 'react-router'; +import { useRouteMatch } from 'react-router-dom'; +import urls from 'routing/urls'; + +import { + useGetHospital, + useGetHospitals, + useGetPatient, + useGetSurgeons, + useRegisterEpisode, +} from '../../hooks/api/patientHooks'; +import RegisterEpisodeForm from './components/RegisterEpisodeForm'; +import { RegisterEpisodeFormType } from './types'; +import { formValidation } from './utils'; + +const RegisterEpisode: React.FC = () => { + const match = useRouteMatch<{ hospitalID?: string; patientID?: string }>(); + const { hospitalID, patientID } = match.params; + + const { data: hospitals, isLoading: isHospitalsLoading } = useGetHospitals({ + offset: 0, + limit: 100, + }); + const { data: patient, isLoading: isPatientLoading } = useGetPatient(patientID ?? ''); + const { data: hospital, isLoading: isHospitalLoading } = useGetHospital(hospitalID ?? ''); + const { data: surgeons, isLoading: isSurgeonsLoading } = useGetSurgeons({ + offset: 0, + limit: 100, + }); + + const hospitalPatientID = patient?.hospital_mappings.find( + (value) => value.hospital_id === hospital?.id + )?.patient_hospital_id; + + const isLoading = + isHospitalLoading || isHospitalsLoading || isSurgeonsLoading || isPatientLoading; + + const { mutate, isLoading: isSubmitLoading } = useRegisterEpisode(hospitalID, patientID); + + const handleSubmit = (form: RegisterEpisodeFormType) => { + mutate(form); + }; + + const [isFormDirty, setIsFormDirty] = useState(false); + const [showWarningModal, setShowWarningModal] = useState(false); + + const history = useHistory(); + + return ( + <> + + + { + if (isFormDirty) { + setShowWarningModal(true); + } else { + history.push(urls.patients()); + } + }} + /> + Register an Episode + + + Please verify that the hospital of the surgery is correct. If you wish, you can choose + another hospital. + +
+ {({ handleSubmit, values, dirty, valid, submitting }) => { + if (dirty) { + setIsFormDirty(true); + } + + return ( + + {patient && surgeons && hospitals && hospital && ( + + )} + + + + + ); + }} + +
+ {showWarningModal && ( + { + setShowWarningModal(false); + }} + title={'Cancel new registration?'} + subtitle={ + 'Are you sure you want to cancel registering an episode? All information you’ve entered will be lost!' + } + buttonText={'Yes, cancel new addition'} + onClick={() => history.push(`${urls.patients()}/${hospitalID}/${patientID}`)} + /> + )} + + ); +}; + +export default RegisterEpisode; diff --git a/src/pages/RegisterEpisode/components/RegisterEpisodeForm/RegisterEpisodeForm.style.tsx b/src/pages/RegisterEpisode/components/RegisterEpisodeForm/RegisterEpisodeForm.style.tsx new file mode 100644 index 0000000..8244c1d --- /dev/null +++ b/src/pages/RegisterEpisode/components/RegisterEpisodeForm/RegisterEpisodeForm.style.tsx @@ -0,0 +1,26 @@ +import styled from '@emotion/styled'; + +import { scrollBar } from '../../../../common.style'; + +export const FormHeadingContainer = styled.div` + display: flex; + flex-direction: column; + margin-bottom: 24px; + row-gap: 12px; +`; + +export const FormContainer = styled.div` + flex-grow: 1; + height: 100%; + margin-bottom: 137px; + overflow-y: auto; + padding: 18px; + + ${scrollBar}; +`; + +export const SelectWrapper = styled.div` + & > div > div { + max-width: unset; + } +`; diff --git a/src/pages/RegisterEpisode/components/RegisterEpisodeForm/RegisterEpisodeForm.tsx b/src/pages/RegisterEpisode/components/RegisterEpisodeForm/RegisterEpisodeForm.tsx new file mode 100644 index 0000000..73e5ff9 --- /dev/null +++ b/src/pages/RegisterEpisode/components/RegisterEpisodeForm/RegisterEpisodeForm.tsx @@ -0,0 +1,350 @@ +/** @jsxImportSource @emotion/react */ +import React from 'react'; + +import { Select, TextArea, TextField } from '@orfium/ictinus'; +import { omit } from 'lodash'; +import { Field } from 'react-final-form'; +import { FieldArray } from 'react-final-form-arrays'; + +import { FieldWrapper, SectionTitle } from '../../../../common.style'; +import { HospitalsAPI, PatientAPI, SurgeonsAPI } from '../../../../models/apiTypes'; +import { + ANAESTHETIC_TYPE_OPTIONS, + CEPOD_OPTIONS, + COMPLEXITY_OPTIONS, + DIATHERMY_USED_OPTIONS, + MESH_TYPE_OPTIONS, + OCCURRENCE_OPTIONS, + SIDE_OPTIONS, + TYPE_OPTIONS, +} from '../../constants'; +import { RegisterEpisodeFormType } from '../../types'; +import { getHospitalOptions, getSurgeonOptions } from '../../utils'; +import { FormContainer, FormHeadingContainer, SelectWrapper } from './RegisterEpisodeForm.style'; + +type Props = { + values: RegisterEpisodeFormType; + patient: PatientAPI; + selectedHospital: HospitalsAPI; + surgeons: SurgeonsAPI[]; + hospitals: HospitalsAPI[]; +}; + +const RegisterEpisodeForm: React.FC = ({ surgeons, hospitals }) => { + const hospitalOptions = getHospitalOptions(hospitals); + return ( + + + Hospital Details + + + {(props) => { + const hasError = props.meta.touched && props.meta.invalid && !props.meta.active; + + return ( + + + + ); + }} + + + + + {(props) => { + const hasError = props.meta.touched && props.meta.invalid && !props.meta.active; + + return ( + + + + ); + }} + + + + + {(props) => { + const hasError = props.meta.touched && props.meta.invalid && !props.meta.active; + + return ( + + + + ); + }} + + + + + + Surgery Details + + value}> + {(props) => { + const hasError = props.meta.touched && props.meta.invalid && !props.meta.active; + return ( + + ); + }} + + + + + {(props) => { + const hasError = props.meta.touched && props.meta.invalid && !props.meta.active; + + return ( + + + + ); + }} + + + + + {(props) => { + const hasError = props.meta.touched && props.meta.invalid && !props.meta.active; + + return ( + + + + ); + }} + + )) + } + + + + {(props) => { + return ( +