From 44f15f3a12b3a4252f70c745b37a1140c732d976 Mon Sep 17 00:00:00 2001 From: Marco Moretti Date: Sun, 3 May 2020 10:40:29 +0200 Subject: [PATCH 1/4] feat(i18n): add italian translation (#2035) --- src/i18n.ts | 4 + src/locales/it/translations/actions/index.ts | 15 +++ .../it/translations/dashboard/index.ts | 5 + src/locales/it/translations/index.ts | 19 ++++ src/locales/it/translations/labs/index.ts | 29 +++++ src/locales/it/translations/patient/index.ts | 100 ++++++++++++++++++ src/locales/it/translations/patients/index.ts | 12 +++ .../it/translations/scheduling/index.ts | 36 +++++++ src/locales/it/translations/sex/index.ts | 8 ++ src/locales/it/translations/states/index.ts | 6 ++ 10 files changed, 234 insertions(+) create mode 100644 src/locales/it/translations/actions/index.ts create mode 100644 src/locales/it/translations/dashboard/index.ts create mode 100644 src/locales/it/translations/index.ts create mode 100644 src/locales/it/translations/labs/index.ts create mode 100644 src/locales/it/translations/patient/index.ts create mode 100644 src/locales/it/translations/patients/index.ts create mode 100644 src/locales/it/translations/scheduling/index.ts create mode 100644 src/locales/it/translations/sex/index.ts create mode 100644 src/locales/it/translations/states/index.ts diff --git a/src/i18n.ts b/src/i18n.ts index 34b4477041..b237cb7da6 100644 --- a/src/i18n.ts +++ b/src/i18n.ts @@ -12,8 +12,12 @@ import translationJA from './locales/ja/translations' import translationPtBR from './locales/ptBr/translations' import translationRU from './locales/ru/translations' import translationZR from './locales/zr/translations' +import translationIT from './locales/it/translations' const resources = { + it: { + translation: translationIT, + }, ar: { translation: translationAR, }, diff --git a/src/locales/it/translations/actions/index.ts b/src/locales/it/translations/actions/index.ts new file mode 100644 index 0000000000..a838f2e142 --- /dev/null +++ b/src/locales/it/translations/actions/index.ts @@ -0,0 +1,15 @@ +export default { + actions: { + label: 'Azioni', + edit: 'Modifica', + save: 'Salva', + update: 'Aggiorna', + complete: 'Completa', + delete: 'Rimuovi', + cancel: 'Annulla', + new: 'Nuovo', + list: 'Lista', + search: 'Cerca', + confirmDelete: 'Conferma cancellazione', + }, +} diff --git a/src/locales/it/translations/dashboard/index.ts b/src/locales/it/translations/dashboard/index.ts new file mode 100644 index 0000000000..feaad5f3ee --- /dev/null +++ b/src/locales/it/translations/dashboard/index.ts @@ -0,0 +1,5 @@ +export default { + dashboard: { + label: 'Dashboard', + }, +} diff --git a/src/locales/it/translations/index.ts b/src/locales/it/translations/index.ts new file mode 100644 index 0000000000..d6bf2a1db2 --- /dev/null +++ b/src/locales/it/translations/index.ts @@ -0,0 +1,19 @@ +import actions from './actions' +import dashboard from './dashboard' +import patient from './patient' +import patients from './patients' +import scheduling from './scheduling' +import states from './states' +import sex from './sex' +import labs from './labs' + +export default { + ...actions, + ...dashboard, + ...patient, + ...patients, + ...scheduling, + ...states, + ...sex, + ...labs, +} diff --git a/src/locales/it/translations/labs/index.ts b/src/locales/it/translations/labs/index.ts new file mode 100644 index 0000000000..5f3a3f5c70 --- /dev/null +++ b/src/locales/it/translations/labs/index.ts @@ -0,0 +1,29 @@ +export default { + labs: { + label: 'Laboratori', + requests: { + label: 'Richieste di laboratorio', + new: 'Nuova richiesta di laboratorio', + view: 'Visualizza richiesta', + cancel: 'Annulla richiesta', + complete: 'Completa', + error: { + unableToRequest: 'Impossibile creare la richiesta.', + unableToComplete: 'Impossibile completare la richiesta.', + typeRequired: 'La tipologia è richiesta.', + resultRequiredToComplete: 'Il risultato è obbligatorio per completare la richiesta.', + }, + }, + lab: { + status: 'Stato', + for: 'Per', + type: 'Tipo', + result: 'Risultato', + notes: 'Note', + requestedOn: 'Richiesta fatta il', + completedOn: 'Completato il', + canceledOn: 'Annullata il', + patient: 'Paziente', + }, + }, +} diff --git a/src/locales/it/translations/patient/index.ts b/src/locales/it/translations/patient/index.ts new file mode 100644 index 0000000000..5acf8c5e6f --- /dev/null +++ b/src/locales/it/translations/patient/index.ts @@ -0,0 +1,100 @@ +export default { + patient: { + code: 'Codice del paziente', + firstName: 'Nome', + lastName: 'Cognome', + suffix: 'Suffisso', + prefix: 'Prefisso', + givenName: 'Nome', + familyName: 'Cognome', + dateOfBirth: 'Data di nascita', + approximateDateOfBirth: 'Data presunta di nascita', + age: 'Anni', + approximateAge: 'Anni presunti', + placeOfBirth: 'Luogo di nascita', + sex: 'Sesso', + phoneNumber: 'Numero di telefono', + email: 'Email', + address: 'Indirizzo', + occupation: 'Occupazione', + type: 'Tipo di paziente', + preferredLanguage: 'Lingua', + basicInformation: 'Informazioni base', + generalInformation: 'Informazioni generali', + contactInformation: 'Informazioni di contatto', + unknownDateOfBirth: 'Sconosciuto', + relatedPerson: 'Relazione di parentela', + relatedPersons: { + error: { + unableToAddRelatedPerson: 'Impossibile aggiungere relazione di parentela.', + relatedPersonRequired: 'Il campo relazione di parentela è obbligatorio.', + relationshipTypeRequired: 'Il tipo di relazione è obbligatorio.', + }, + label: 'Relazioni di parentela', + new: 'Nuova relazione di parentela', + add: 'Aggiungi relazione di parentela', + relationshipType: 'Tipo di relazione', + warning: { + noRelatedPersons: 'Non ci sono relazioni di parentela', + }, + addRelatedPersonAbove: + 'Aggiungi una nuova relazione di parentela usando il pulsante soprastante.', + }, + appointments: { + new: 'Aggiungi appuntamento', + }, + allergies: { + label: 'Allergie', + allergyName: "Nome dell'allergia", + new: 'Aggiungi allergia', + error: { + nameRequired: 'Il nome è obbligatorio.', + unableToAdd: "Impossibile aggiungere l'allergia.", + }, + warning: { + noAllergies: 'Non ci sono allergie', + }, + addAllergyAbove: 'Aggiungi un allergia utilizzando il pulsante soprastante.', + successfullyAdded: 'Allergia aggiunta con successo!', + }, + diagnoses: { + label: 'Diagnosi', + new: 'Aggiungi diagnosi', + diagnosisName: 'Nome della diagnosi', + diagnosisDate: 'Data della diagnosi', + warning: { + noDiagnoses: 'Non ci sono diagnosi', + }, + error: { + nameRequired: 'Il nome della diagnosi è obbligatorio.', + dateRequired: 'La data della diagnosi è obbligatorio.', + unableToAdd: 'Impossibile aggiungere la diagnosi', + }, + addDiagnosisAbove: 'Aggiungi la diagnosi utilizzando il pulsante soprastante.', + successfullyAdded: 'Diagnosi aggiunta con successo!', + }, + note: 'Nota', + notes: { + label: 'Note', + new: 'Aggiungi nuova nota', + warning: { + noNotes: 'Non ci sono note', + }, + error: { + noteRequired: 'Il campo nota è obbligatorio.', + unableToAdd: 'Impossibile aggiungere la nota.', + }, + addNoteAbove: 'Aggiungi una nota utilizzando il pulsante soprastante.', + }, + types: { + charity: 'Beneficenza', + private: 'Privato', + }, + errors: { + createPatientError: 'Impossibile aggiungere il paziente.', + updatePatientError: 'Impossibile aggiornare il paziente.', + patientGivenNameFeedback: 'Il nome è obbligatorio.', + patientDateOfBirthFeedback: 'La data di nascita non può essere maggiore della data odierna', + }, + }, +} diff --git a/src/locales/it/translations/patients/index.ts b/src/locales/it/translations/patients/index.ts new file mode 100644 index 0000000000..ea6d2d5190 --- /dev/null +++ b/src/locales/it/translations/patients/index.ts @@ -0,0 +1,12 @@ +export default { + patients: { + label: 'Pazienti', + patientsList: 'Lista Pazienti', + viewPatients: 'Vedi Pazienti', + viewPatient: 'Vedi Paziente', + newPatient: 'Nuovo Paziente', + successfullyCreated: 'Paziente creato con successo', + successfullyAddedNote: 'Note aggiunte con successo', + successfullyAddedRelatedPerson: 'Relazione di parentela aggiunta con successo', + }, +} diff --git a/src/locales/it/translations/scheduling/index.ts b/src/locales/it/translations/scheduling/index.ts new file mode 100644 index 0000000000..891d2f41c2 --- /dev/null +++ b/src/locales/it/translations/scheduling/index.ts @@ -0,0 +1,36 @@ +export default { + scheduling: { + label: 'Appuntamenti', + appointments: { + label: 'Appuntamenti', + new: 'Nuovo appuntamento', + schedule: 'Programma appuntamento', + editAppointment: 'Modifica appuntamento', + deleteAppointment: 'Cancella appuntamento', + viewAppointment: 'Vedi appuntamento', + }, + appointment: { + startDate: 'Data di inizio', + endDate: 'Data di fine', + location: 'Luogo', + type: 'Tipo', + types: { + checkup: 'Checkup', + emergency: 'Emergenza', + followUp: 'Approfondimento', + routine: 'Routine', + walkIn: 'Senza appuntamento', + }, + errors: { + patientRequired: 'Il paziente è obbligatorio.', + errorCreatingAppointment: "Impossibile creare l'appuntamento!", + startDateMustBeBeforeEndDate: + 'La data di inizio non può essere inferiore a quella di fine.', + }, + reason: 'Motivo', + patient: 'Paziente', + deleteConfirmationMessage: "Sei sicuro di voler cancellare l'appuntamento?", + successfullyCreated: 'Appuntamento creato con successo.', + }, + }, +} diff --git a/src/locales/it/translations/sex/index.ts b/src/locales/it/translations/sex/index.ts new file mode 100644 index 0000000000..8979536b05 --- /dev/null +++ b/src/locales/it/translations/sex/index.ts @@ -0,0 +1,8 @@ +export default { + sex: { + male: 'Maschio', + female: 'Femmina', + other: 'Altro', + unknown: 'Sconosciuto', + }, +} diff --git a/src/locales/it/translations/states/index.ts b/src/locales/it/translations/states/index.ts new file mode 100644 index 0000000000..134cd45889 --- /dev/null +++ b/src/locales/it/translations/states/index.ts @@ -0,0 +1,6 @@ +export default { + states: { + success: 'Successo!', + error: 'Errore!', + }, +} From bb02fa20a23eab285c88cfe6ef16d28783d24ec6 Mon Sep 17 00:00:00 2001 From: Lucas Date: Sun, 3 May 2020 18:42:22 +0100 Subject: [PATCH 2/4] feat(patient): add input validation (#2032) --- src/__tests__/patients/patient-slice.test.ts | 39 +++++++++++++++++++ .../enUs/translations/patient/index.ts | 4 ++ src/patients/GeneralInformation.tsx | 8 ++++ src/patients/patient-slice.ts | 30 ++++++++++++++ 4 files changed, 81 insertions(+) diff --git a/src/__tests__/patients/patient-slice.test.ts b/src/__tests__/patients/patient-slice.test.ts index 28ed65761c..dba7899dcd 100644 --- a/src/__tests__/patients/patient-slice.test.ts +++ b/src/__tests__/patients/patient-slice.test.ts @@ -295,6 +295,37 @@ describe('patients slice', () => { }), ) }) + + it('should validate fields that should only contian alpha characters', async () => { + const store = mockStore() + const expectedPatientId = 'sliceId10' + const expectedPatient = { + id: expectedPatientId, + givenName: 'some given name', + suffix: 'A123', + familyName: 'B456', + prefix: 'C987', + preferredLanguage: 'D321', + } as Patient + const saveOrUpdateSpy = jest + .spyOn(PatientRepository, 'saveOrUpdate') + .mockResolvedValue(expectedPatient) + const onSuccessSpy = jest.fn() + + await store.dispatch(createPatient(expectedPatient, onSuccessSpy)) + + expect(onSuccessSpy).not.toHaveBeenCalled() + expect(saveOrUpdateSpy).not.toHaveBeenCalled() + expect(store.getActions()[1]).toEqual( + createPatientError({ + message: 'patient.errors.createPatientError', + suffix: 'patient.errors.patientNumInSuffixFeedback', + familyName: 'patient.errors.patientNumInFamilyNameFeedback', + prefix: 'patient.errors.patientNumInPrefixFeedback', + preferredLanguage: 'patient.errors.patientNumInPreferredLanguageFeedback', + }), + ) + }) }) describe('fetch patient', () => { @@ -386,6 +417,10 @@ describe('patients slice', () => { id: expectedPatientId, givenName: undefined, dateOfBirth: addDays(new Date(), 4).toISOString(), + suffix: '061002', + prefix: '061002', + familyName: '061002', + preferredLanguage: '061002', } as Patient const saveOrUpdateSpy = jest .spyOn(PatientRepository, 'saveOrUpdate') @@ -401,6 +436,10 @@ describe('patients slice', () => { message: 'patient.errors.updatePatientError', givenName: 'patient.errors.patientGivenNameFeedback', dateOfBirth: 'patient.errors.patientDateOfBirthFeedback', + suffix: 'patient.errors.patientNumInSuffixFeedback', + familyName: 'patient.errors.patientNumInFamilyNameFeedback', + prefix: 'patient.errors.patientNumInPrefixFeedback', + preferredLanguage: 'patient.errors.patientNumInPreferredLanguageFeedback', }), ) }) diff --git a/src/locales/enUs/translations/patient/index.ts b/src/locales/enUs/translations/patient/index.ts index 32b6b4b331..c404f9e229 100644 --- a/src/locales/enUs/translations/patient/index.ts +++ b/src/locales/enUs/translations/patient/index.ts @@ -94,6 +94,10 @@ export default { updatePatientError: 'Could not update patient.', patientGivenNameFeedback: 'Given Name is required.', patientDateOfBirthFeedback: 'Date of Birth can not be greater than today', + patientNumInSuffixFeedback: 'Cannot contain numbers.', + patientNumInPrefixFeedback: 'Cannot contain numbers.', + patientNumInFamilyNameFeedback: 'Cannot contain numbers.', + patientNumInPreferredLanguageFeedback: 'Cannot contain numbers.', invalidEmail: 'Must be a valid email.', invalidPhoneNumber: 'Must be a valid phone number.', }, diff --git a/src/patients/GeneralInformation.tsx b/src/patients/GeneralInformation.tsx index 6f926a6d2d..ca9a7f1f2c 100644 --- a/src/patients/GeneralInformation.tsx +++ b/src/patients/GeneralInformation.tsx @@ -60,6 +60,8 @@ const GeneralInformation = (props: Props) => { onChange={(event: React.ChangeEvent) => { onInputElementChange(event, 'prefix') }} + isInvalid={error?.prefix} + feedback={t(error?.prefix)} />
@@ -85,6 +87,8 @@ const GeneralInformation = (props: Props) => { onChange={(event: React.ChangeEvent) => { onInputElementChange(event, 'familyName') }} + isInvalid={error?.familyName} + feedback={t(error?.familyName)} />
@@ -96,6 +100,8 @@ const GeneralInformation = (props: Props) => { onChange={(event: React.ChangeEvent) => { onInputElementChange(event, 'suffix') }} + isInvalid={error?.suffix} + feedback={t(error?.suffix)} />
@@ -195,6 +201,8 @@ const GeneralInformation = (props: Props) => { onChange={(event: React.ChangeEvent) => { onInputElementChange(event, 'preferredLanguage') }} + isInvalid={error?.preferredLanguage} + feedback={t(error?.preferredLanguage)} /> diff --git a/src/patients/patient-slice.ts b/src/patients/patient-slice.ts index 4488ef5eef..ba79c89dc9 100644 --- a/src/patients/patient-slice.ts +++ b/src/patients/patient-slice.ts @@ -28,6 +28,10 @@ interface Error { message?: string givenName?: string dateOfBirth?: string + suffix?: string + prefix?: string + familyName?: string + preferredLanguage?: string email?: string phoneNumber?: string } @@ -141,6 +145,8 @@ export const fetchPatient = (id: string): AppThunk => async (dispatch) => { function validatePatient(patient: Patient) { const error: Error = {} + const regexContainsNumber = /\d/ + if (!patient.givenName) { error.givenName = 'patient.errors.patientGivenNameFeedback' } @@ -153,6 +159,30 @@ function validatePatient(patient: Patient) { } } + if (patient.suffix) { + if (regexContainsNumber.test(patient.suffix)) { + error.suffix = 'patient.errors.patientNumInSuffixFeedback' + } + } + + if (patient.prefix) { + if (regexContainsNumber.test(patient.prefix)) { + error.prefix = 'patient.errors.patientNumInPrefixFeedback' + } + } + + if (patient.familyName) { + if (regexContainsNumber.test(patient.familyName)) { + error.familyName = 'patient.errors.patientNumInFamilyNameFeedback' + } + } + + if (patient.preferredLanguage) { + if (regexContainsNumber.test(patient.preferredLanguage)) { + error.preferredLanguage = 'patient.errors.patientNumInPreferredLanguageFeedback' + } + } + if (patient.email) { if (!validator.isEmail(patient.email)) { error.email = 'patient.errors.invalidEmail' From 4a1c7ed4a80265e55020f8b86fbec1aedf366330 Mon Sep 17 00:00:00 2001 From: Ruben Guarachi Torres Date: Sun, 3 May 2020 19:54:22 +0200 Subject: [PATCH 3/4] feat(viewpatient): added labs tab to ViewPatient (#1987) --- package.json | 1 + src/__tests__/patients/labs/LabsTab.test.tsx | 94 +++++++++++++++++++ .../patients/view/ViewPatient.test.tsx | 31 +++++- .../appointments/new/NewAppointment.test.tsx | 3 + src/clients/db/LabRepository.ts | 12 +++ .../enUs/translations/patient/index.ts | 8 ++ src/patients/labs/LabsTab.tsx | 66 +++++++++++++ src/patients/view/ViewPatient.tsx | 9 ++ 8 files changed, 222 insertions(+), 2 deletions(-) create mode 100644 src/__tests__/patients/labs/LabsTab.test.tsx create mode 100644 src/patients/labs/LabsTab.tsx diff --git a/package.json b/package.json index 105ca34417..5a14ebd333 100644 --- a/package.json +++ b/package.json @@ -7,6 +7,7 @@ "dependencies": { "@hospitalrun/components": "^1.4.0", "@reduxjs/toolkit": "~1.3.0", + "@types/escape-string-regexp": "~2.0.1", "@types/pouchdb-find": "~6.3.4", "bootstrap": "~4.4.1", "date-fns": "~2.12.0", diff --git a/src/__tests__/patients/labs/LabsTab.test.tsx b/src/__tests__/patients/labs/LabsTab.test.tsx new file mode 100644 index 0000000000..cc5f5c0518 --- /dev/null +++ b/src/__tests__/patients/labs/LabsTab.test.tsx @@ -0,0 +1,94 @@ +import '../../../__mocks__/matchMediaMock' +import React from 'react' +import configureMockStore from 'redux-mock-store' +import thunk from 'redux-thunk' +import { mount } from 'enzyme' +import { createMemoryHistory } from 'history' +import { Router } from 'react-router' +import { Provider } from 'react-redux' +import * as components from '@hospitalrun/components' +import format from 'date-fns/format' +import { act } from 'react-dom/test-utils' +import LabsTab from '../../../patients/labs/LabsTab' +import Patient from '../../../model/Patient' +import Lab from '../../../model/Lab' +import Permissions from '../../../model/Permissions' +import LabRepository from '../../../clients/db/LabRepository' + +const expectedPatient = { + id: '123', +} as Patient + +const labs = [ + { + id: 'labId', + patientId: '123', + type: 'type', + status: 'requested', + requestedOn: new Date().toISOString(), + } as Lab, +] + +const mockStore = configureMockStore([thunk]) +const history = createMemoryHistory() + +let user: any +let store: any + +const setup = (patient = expectedPatient, permissions = [Permissions.WritePatients]) => { + user = { permissions } + store = mockStore({ patient, user }) + jest.spyOn(LabRepository, 'findAllByPatientId').mockResolvedValue(labs) + const wrapper = mount( + + + + + , + ) + + return wrapper +} + +describe('Labs Tab', () => { + it('should list the patients labs', async () => { + const expectedLabs = labs + let wrapper: any + await act(async () => { + wrapper = await setup() + }) + wrapper.update() + + const table = wrapper.find('table') + const tableHeader = wrapper.find('thead') + const tableHeaders = wrapper.find('th') + const tableBody = wrapper.find('tbody') + const tableData = wrapper.find('td') + + expect(table).toHaveLength(1) + expect(tableHeader).toHaveLength(1) + expect(tableBody).toHaveLength(1) + expect(tableHeaders.at(0).text()).toEqual('labs.lab.type') + expect(tableHeaders.at(1).text()).toEqual('labs.lab.requestedOn') + expect(tableHeaders.at(2).text()).toEqual('labs.lab.status') + expect(tableData.at(0).text()).toEqual(expectedLabs[0].type) + expect(tableData.at(1).text()).toEqual( + format(new Date(expectedLabs[0].requestedOn), 'yyyy-MM-dd hh:mm a'), + ) + expect(tableData.at(2).text()).toEqual(expectedLabs[0].status) + }) + + it('should render a warning message if the patient does not have any labs', async () => { + let wrapper: any + + await act(async () => { + wrapper = await setup({ ...expectedPatient }) + }) + + const alert = wrapper.find(components.Alert) + + expect(alert).toHaveLength(1) + expect(alert.prop('title')).toEqual('patient.labs.warning.noLabs') + expect(alert.prop('message')).toEqual('patient.labs.noLabsMessage') + }) +}) diff --git a/src/__tests__/patients/view/ViewPatient.test.tsx b/src/__tests__/patients/view/ViewPatient.test.tsx index e24c07fcdf..3080e73384 100644 --- a/src/__tests__/patients/view/ViewPatient.test.tsx +++ b/src/__tests__/patients/view/ViewPatient.test.tsx @@ -21,6 +21,8 @@ import * as titleUtil from '../../../page-header/useTitle' import ViewPatient from '../../../patients/view/ViewPatient' import * as patientSlice from '../../../patients/patient-slice' import Permissions from '../../../model/Permissions' +import LabsTab from '../../../patients/labs/LabsTab' +import LabRepository from '../../../clients/db/LabRepository' const mockStore = configureMockStore([thunk]) @@ -47,9 +49,9 @@ describe('ViewPatient', () => { const setup = (permissions = [Permissions.ReadPatients]) => { jest.spyOn(PatientRepository, 'find') + jest.spyOn(LabRepository, 'findAllByPatientId').mockResolvedValue([]) const mockedPatientRepository = mocked(PatientRepository, true) mockedPatientRepository.find.mockResolvedValue(patient) - history = createMemoryHistory() store = mockStore({ patient: { patient }, @@ -127,13 +129,14 @@ describe('ViewPatient', () => { const tabs = tabsHeader.find(Tab) expect(tabsHeader).toHaveLength(1) - expect(tabs).toHaveLength(6) + expect(tabs).toHaveLength(7) expect(tabs.at(0).prop('label')).toEqual('patient.generalInformation') expect(tabs.at(1).prop('label')).toEqual('patient.relatedPersons.label') expect(tabs.at(2).prop('label')).toEqual('scheduling.appointments.label') expect(tabs.at(3).prop('label')).toEqual('patient.allergies.label') expect(tabs.at(4).prop('label')).toEqual('patient.diagnoses.label') expect(tabs.at(5).prop('label')).toEqual('patient.notes.label') + expect(tabs.at(6).prop('label')).toEqual('patient.labs.label') }) it('should mark the general information tab as active and render the general information component when route is /patients/:id', async () => { @@ -262,4 +265,28 @@ describe('ViewPatient', () => { expect(notesTab).toHaveLength(1) expect(notesTab.prop('patient')).toEqual(patient) }) + + it('should mark the labs tab as active when it is clicked and render the lab component when route is /patients/:id/labs', async () => { + let wrapper: any + await act(async () => { + wrapper = await setup() + }) + + await act(async () => { + const tabsHeader = wrapper.find(TabsHeader) + const tabs = tabsHeader.find(Tab) + tabs.at(6).prop('onClick')() + }) + + wrapper.update() + + const tabsHeader = wrapper.find(TabsHeader) + const tabs = tabsHeader.find(Tab) + const labsTab = wrapper.find(LabsTab) + + expect(history.location.pathname).toEqual(`/patients/${patient.id}/labs`) + expect(tabs.at(6).prop('active')).toBeTruthy() + expect(labsTab).toHaveLength(1) + expect(labsTab.prop('patientId')).toEqual(patient.id) + }) }) diff --git a/src/__tests__/scheduling/appointments/new/NewAppointment.test.tsx b/src/__tests__/scheduling/appointments/new/NewAppointment.test.tsx index 4b502dcd41..7d79ec1b67 100644 --- a/src/__tests__/scheduling/appointments/new/NewAppointment.test.tsx +++ b/src/__tests__/scheduling/appointments/new/NewAppointment.test.tsx @@ -18,6 +18,8 @@ import AppointmentDetailForm from 'scheduling/appointments/AppointmentDetailForm import * as components from '@hospitalrun/components' import * as titleUtil from '../../../../page-header/useTitle' import * as appointmentSlice from '../../../../scheduling/appointments/appointment-slice' +import LabRepository from '../../../../clients/db/LabRepository' +import Lab from '../../../../model/Lab' const mockStore = configureMockStore([thunk]) const mockedComponents = mocked(components, true) @@ -32,6 +34,7 @@ describe('New Appointment', () => { mocked(AppointmentRepository, true).save.mockResolvedValue( expectedNewAppointment as Appointment, ) + jest.spyOn(LabRepository, 'findAllByPatientId').mockResolvedValue([] as Lab[]) history = createMemoryHistory() store = mockStore({ diff --git a/src/clients/db/LabRepository.ts b/src/clients/db/LabRepository.ts index 6ffbfdd6d3..9ddfa74df0 100644 --- a/src/clients/db/LabRepository.ts +++ b/src/clients/db/LabRepository.ts @@ -9,6 +9,18 @@ export class LabRepository extends Repository { index: { fields: ['requestedOn'] }, }) } + + async findAllByPatientId(patientId: string): Promise { + return super.search({ + selector: { + $and: [ + { + patientId, + }, + ], + }, + }) + } } export default new LabRepository() diff --git a/src/locales/enUs/translations/patient/index.ts b/src/locales/enUs/translations/patient/index.ts index c404f9e229..5bbbedc04d 100644 --- a/src/locales/enUs/translations/patient/index.ts +++ b/src/locales/enUs/translations/patient/index.ts @@ -85,6 +85,14 @@ export default { }, addNoteAbove: 'Add a note using the button above.', }, + labs: { + label: 'Labs', + new: 'Add New Lab', + warning: { + noLabs: 'No Labs', + }, + noLabsMessage: 'No labs requests for this person.', + }, types: { charity: 'Charity', private: 'Private', diff --git a/src/patients/labs/LabsTab.tsx b/src/patients/labs/LabsTab.tsx new file mode 100644 index 0000000000..6b40d81353 --- /dev/null +++ b/src/patients/labs/LabsTab.tsx @@ -0,0 +1,66 @@ +import React, { useEffect, useState } from 'react' +import { Alert } from '@hospitalrun/components' +import { useTranslation } from 'react-i18next' +import format from 'date-fns/format' +import { useHistory } from 'react-router' +import Lab from '../../model/Lab' +import LabRepository from '../../clients/db/LabRepository' + +interface Props { + patientId: string +} + +const LabsTab = (props: Props) => { + const history = useHistory() + const { patientId } = props + const { t } = useTranslation() + + const [labs, setLabs] = useState([]) + + useEffect(() => { + const fetch = async () => { + const fetchedLabs = await LabRepository.findAllByPatientId(patientId) + setLabs(fetchedLabs) + } + + fetch() + }, [patientId]) + + const onTableRowClick = (lab: Lab) => { + history.push(`/labs/${lab.id}`) + } + + return ( +
+ {(!labs || labs.length === 0) && ( + + )} + {labs && labs.length > 0 && ( + + + + + + + + + + {labs.map((lab) => ( + onTableRowClick(lab)} key={lab.id}> + + + + + ))} + +
{t('labs.lab.type')}{t('labs.lab.requestedOn')}{t('labs.lab.status')}
{lab.type}{format(new Date(lab.requestedOn), 'yyyy-MM-dd hh:mm a')}{lab.status}
+ )} +
+ ) +} + +export default LabsTab diff --git a/src/patients/view/ViewPatient.tsx b/src/patients/view/ViewPatient.tsx index fab29a157c..6119142e0b 100644 --- a/src/patients/view/ViewPatient.tsx +++ b/src/patients/view/ViewPatient.tsx @@ -18,6 +18,7 @@ import RelatedPerson from '../related-persons/RelatedPersonTab' import useAddBreadcrumbs from '../../breadcrumbs/useAddBreadcrumbs' import AppointmentsList from '../appointments/AppointmentsList' import Note from '../notes/NoteTab' +import Labs from '../labs/LabsTab' const getPatientCode = (p: Patient): string => { if (p) { @@ -113,6 +114,11 @@ const ViewPatient = () => { label={t('patient.notes.label')} onClick={() => history.push(`/patients/${patient.id}/notes`)} /> + history.push(`/patients/${patient.id}/labs`)} + /> @@ -133,6 +139,9 @@ const ViewPatient = () => { + + + ) From b695ac899ea73e7059a50ad633bea90932431ff8 Mon Sep 17 00:00:00 2001 From: Marco Moretti Date: Sun, 3 May 2020 20:43:09 +0200 Subject: [PATCH 4/4] feat(labs): add lab code (#2040) --- src/__tests__/clients/db/LabRepository.test.ts | 14 ++++++++++++++ src/__tests__/labs/ViewLab.test.tsx | 5 ++++- src/__tests__/labs/ViewLabs.test.tsx | 17 +++++++++++------ src/__tests__/utils/generateCode.test.ts | 6 ++++++ src/clients/db/LabRepository.ts | 7 +++++++ src/clients/db/PatientRepository.ts | 8 ++------ src/labs/ViewLab.tsx | 2 +- src/labs/ViewLabs.tsx | 2 ++ src/locales/ar/translations/labs/index.ts | 7 +++++++ src/locales/de/translations/labs/index.ts | 7 +++++++ src/locales/enUs/translations/labs/index.ts | 1 + src/locales/es/translations/labs/index.ts | 7 +++++++ src/locales/fr/translations/labs/index.ts | 1 + src/locales/in/translations/labs/index.ts | 7 +++++++ src/locales/ja/translations/labs/index.ts | 7 +++++++ src/locales/ptBr/translations/labs/index.ts | 1 + src/locales/ru/translations/labs/index.ts | 7 +++++++ src/locales/zr/translations/labs/index.ts | 7 +++++++ src/model/Lab.ts | 1 + src/util/generateCode.ts | 8 ++++++++ 20 files changed, 108 insertions(+), 14 deletions(-) create mode 100644 src/__tests__/clients/db/LabRepository.test.ts create mode 100644 src/__tests__/utils/generateCode.test.ts create mode 100644 src/locales/ar/translations/labs/index.ts create mode 100644 src/locales/de/translations/labs/index.ts create mode 100644 src/locales/es/translations/labs/index.ts create mode 100644 src/locales/in/translations/labs/index.ts create mode 100644 src/locales/ja/translations/labs/index.ts create mode 100644 src/locales/ru/translations/labs/index.ts create mode 100644 src/locales/zr/translations/labs/index.ts create mode 100644 src/util/generateCode.ts diff --git a/src/__tests__/clients/db/LabRepository.test.ts b/src/__tests__/clients/db/LabRepository.test.ts new file mode 100644 index 0000000000..e7f24726d5 --- /dev/null +++ b/src/__tests__/clients/db/LabRepository.test.ts @@ -0,0 +1,14 @@ +import shortid from 'shortid' +import LabRepository from '../../../clients/db/LabRepository' +import Lab from '../../../model/Lab' + +describe('lab repository', () => { + it('should generate a lab code', async () => { + const newLab = await LabRepository.save({ + patientId: '123', + type: 'test', + } as Lab) + + expect(shortid.isValid(newLab.code)).toBeTruthy() + }) +}) diff --git a/src/__tests__/labs/ViewLab.test.tsx b/src/__tests__/labs/ViewLab.test.tsx index 97f17faff5..b5cbd154e2 100644 --- a/src/__tests__/labs/ViewLab.test.tsx +++ b/src/__tests__/labs/ViewLab.test.tsx @@ -25,6 +25,7 @@ describe('View Labs', () => { let history: any const mockPatient = { fullName: 'test' } const mockLab = { + code: 'L-1234', id: '12456', status: 'requested', patientId: '1234', @@ -83,7 +84,9 @@ describe('View Labs', () => { it('should set the title', async () => { await setup(mockLab, [Permissions.ViewLab]) - expect(titleSpy).toHaveBeenCalledWith(`${mockLab.type} for ${mockPatient.fullName}`) + expect(titleSpy).toHaveBeenCalledWith( + `${mockLab.type} for ${mockPatient.fullName}(${mockLab.code})`, + ) }) describe('page content', () => { diff --git a/src/__tests__/labs/ViewLabs.test.tsx b/src/__tests__/labs/ViewLabs.test.tsx index 89c2e9f8c4..a1c22b4c12 100644 --- a/src/__tests__/labs/ViewLabs.test.tsx +++ b/src/__tests__/labs/ViewLabs.test.tsx @@ -94,6 +94,7 @@ describe('View Labs', () => { let wrapper: ReactWrapper let history: any const expectedLab = { + code: 'L-1234', id: '1234', type: 'lab type', patientId: 'patientId', @@ -133,19 +134,23 @@ describe('View Labs', () => { expect(table).toBeDefined() expect(tableHeader).toBeDefined() expect(tableBody).toBeDefined() - expect(tableColumnHeaders.at(0).text().trim()).toEqual('labs.lab.type') + expect(tableColumnHeaders.at(0).text().trim()).toEqual('labs.lab.code') - expect(tableColumnHeaders.at(1).text().trim()).toEqual('labs.lab.requestedOn') + expect(tableColumnHeaders.at(1).text().trim()).toEqual('labs.lab.type') - expect(tableColumnHeaders.at(2).text().trim()).toEqual('labs.lab.status') + expect(tableColumnHeaders.at(2).text().trim()).toEqual('labs.lab.requestedOn') - expect(tableDataColumns.at(0).text().trim()).toEqual(expectedLab.type) + expect(tableColumnHeaders.at(3).text().trim()).toEqual('labs.lab.status') - expect(tableDataColumns.at(1).text().trim()).toEqual( + expect(tableDataColumns.at(0).text().trim()).toEqual(expectedLab.code) + + expect(tableDataColumns.at(1).text().trim()).toEqual(expectedLab.type) + + expect(tableDataColumns.at(2).text().trim()).toEqual( format(new Date(expectedLab.requestedOn), 'yyyy-MM-dd hh:mm a'), ) - expect(tableDataColumns.at(2).text().trim()).toEqual(expectedLab.status) + expect(tableDataColumns.at(3).text().trim()).toEqual(expectedLab.status) }) it('should navigate to the lab when the row is clicked', () => { diff --git a/src/__tests__/utils/generateCode.test.ts b/src/__tests__/utils/generateCode.test.ts new file mode 100644 index 0000000000..22a44dd2a5 --- /dev/null +++ b/src/__tests__/utils/generateCode.test.ts @@ -0,0 +1,6 @@ +import generateCode from '../../util/generateCode' + +it('should generate a code with prefix A-', () => { + const generatedCode = generateCode('A') + expect(generatedCode).toMatch(/^A-/) +}) diff --git a/src/clients/db/LabRepository.ts b/src/clients/db/LabRepository.ts index 9ddfa74df0..6748ab1e7d 100644 --- a/src/clients/db/LabRepository.ts +++ b/src/clients/db/LabRepository.ts @@ -1,4 +1,5 @@ import Lab from 'model/Lab' +import generateCode from '../../util/generateCode' import Repository from './Repository' import { labs } from '../../config/pouchdb' @@ -10,6 +11,12 @@ export class LabRepository extends Repository { }) } + async save(entity: Lab): Promise { + const labCode = generateCode('L') + entity.code = labCode + return super.save(entity) + } + async findAllByPatientId(patientId: string): Promise { return super.search({ selector: { diff --git a/src/clients/db/PatientRepository.ts b/src/clients/db/PatientRepository.ts index 99a239685e..172e380a7f 100644 --- a/src/clients/db/PatientRepository.ts +++ b/src/clients/db/PatientRepository.ts @@ -1,13 +1,9 @@ import escapeStringRegexp from 'escape-string-regexp' -import shortid from 'shortid' import Patient from '../../model/Patient' +import generateCode from '../../util/generateCode' import Repository from './Repository' import { patients } from '../../config/pouchdb' -const formatPatientCode = (prefix: string, sequenceNumber: string) => `${prefix}${sequenceNumber}` - -const getPatientCode = (): string => formatPatientCode('P-', shortid.generate()) - export class PatientRepository extends Repository { constructor() { super(patients) @@ -32,7 +28,7 @@ export class PatientRepository extends Repository { } async save(entity: Patient): Promise { - const patientCode = getPatientCode() + const patientCode = generateCode('P') entity.code = patientCode return super.save(entity) } diff --git a/src/labs/ViewLab.tsx b/src/labs/ViewLab.tsx index 57c72b860b..6f58f2c9e6 100644 --- a/src/labs/ViewLab.tsx +++ b/src/labs/ViewLab.tsx @@ -14,7 +14,7 @@ import { RootState } from '../store' import { cancelLab, completeLab, updateLab, fetchLab } from './lab-slice' const getTitle = (patient: Patient | undefined, lab: Lab | undefined) => - patient && lab ? `${lab.type} for ${patient.fullName}` : '' + patient && lab ? `${lab.type} for ${patient.fullName}(${lab.code})` : '' const ViewLab = () => { const { id } = useParams() diff --git a/src/labs/ViewLabs.tsx b/src/labs/ViewLabs.tsx index 0e68270f44..42909a9537 100644 --- a/src/labs/ViewLabs.tsx +++ b/src/labs/ViewLabs.tsx @@ -72,6 +72,7 @@ const ViewLabs = () => { + @@ -80,6 +81,7 @@ const ViewLabs = () => { {labs.map((lab) => ( onTableRowClick(lab)} key={lab.id}> + diff --git a/src/locales/ar/translations/labs/index.ts b/src/locales/ar/translations/labs/index.ts new file mode 100644 index 0000000000..f0cc9a23d0 --- /dev/null +++ b/src/locales/ar/translations/labs/index.ts @@ -0,0 +1,7 @@ +export default { + labs: { + lab: { + code: 'كود المختبر', + }, + }, +} diff --git a/src/locales/de/translations/labs/index.ts b/src/locales/de/translations/labs/index.ts new file mode 100644 index 0000000000..6bdaf43580 --- /dev/null +++ b/src/locales/de/translations/labs/index.ts @@ -0,0 +1,7 @@ +export default { + labs: { + lab: { + code: 'Laborcode', + }, + }, +} diff --git a/src/locales/enUs/translations/labs/index.ts b/src/locales/enUs/translations/labs/index.ts index 691424c04e..ae22c88c02 100644 --- a/src/locales/enUs/translations/labs/index.ts +++ b/src/locales/enUs/translations/labs/index.ts @@ -15,6 +15,7 @@ export default { }, }, lab: { + code: 'Lab Code', status: 'Status', for: 'For', type: 'Type', diff --git a/src/locales/es/translations/labs/index.ts b/src/locales/es/translations/labs/index.ts new file mode 100644 index 0000000000..e7f7d6c8e9 --- /dev/null +++ b/src/locales/es/translations/labs/index.ts @@ -0,0 +1,7 @@ +export default { + labs: { + lab: { + code: 'Código', + }, + }, +} diff --git a/src/locales/fr/translations/labs/index.ts b/src/locales/fr/translations/labs/index.ts index 83c6934752..8123ce3003 100644 --- a/src/locales/fr/translations/labs/index.ts +++ b/src/locales/fr/translations/labs/index.ts @@ -15,6 +15,7 @@ export default { }, }, lab: { + code: 'Code', status: 'Statut', for: 'Pour', type: 'Type', diff --git a/src/locales/in/translations/labs/index.ts b/src/locales/in/translations/labs/index.ts new file mode 100644 index 0000000000..2297a1bd24 --- /dev/null +++ b/src/locales/in/translations/labs/index.ts @@ -0,0 +1,7 @@ +export default { + labs: { + lab: { + code: 'Kode', + }, + }, +} diff --git a/src/locales/ja/translations/labs/index.ts b/src/locales/ja/translations/labs/index.ts new file mode 100644 index 0000000000..1db4e26a65 --- /dev/null +++ b/src/locales/ja/translations/labs/index.ts @@ -0,0 +1,7 @@ +export default { + labs: { + lab: { + code: '検査コード', + }, + }, +} diff --git a/src/locales/ptBr/translations/labs/index.ts b/src/locales/ptBr/translations/labs/index.ts index 94be005b4d..f4b384e36d 100644 --- a/src/locales/ptBr/translations/labs/index.ts +++ b/src/locales/ptBr/translations/labs/index.ts @@ -15,6 +15,7 @@ export default { }, }, lab: { + code: 'Código', status: 'Estado', for: 'Por', type: 'Tipo', diff --git a/src/locales/ru/translations/labs/index.ts b/src/locales/ru/translations/labs/index.ts new file mode 100644 index 0000000000..1c889914f5 --- /dev/null +++ b/src/locales/ru/translations/labs/index.ts @@ -0,0 +1,7 @@ +export default { + labs: { + lab: { + code: 'Лабораторный код', + }, + }, +} diff --git a/src/locales/zr/translations/labs/index.ts b/src/locales/zr/translations/labs/index.ts new file mode 100644 index 0000000000..1c889914f5 --- /dev/null +++ b/src/locales/zr/translations/labs/index.ts @@ -0,0 +1,7 @@ +export default { + labs: { + lab: { + code: 'Лабораторный код', + }, + }, +} diff --git a/src/model/Lab.ts b/src/model/Lab.ts index 0c9d0e22f4..64b17ce431 100644 --- a/src/model/Lab.ts +++ b/src/model/Lab.ts @@ -1,6 +1,7 @@ import AbstractDBModel from './AbstractDBModel' export default interface Lab extends AbstractDBModel { + code: string patientId: string type: string notes?: string diff --git a/src/util/generateCode.ts b/src/util/generateCode.ts new file mode 100644 index 0000000000..f476809c1a --- /dev/null +++ b/src/util/generateCode.ts @@ -0,0 +1,8 @@ +import shortid from 'shortid' + +const generateCode = (prefix: string) => { + const id = shortid.generate() + return `${prefix}-${id}` +} + +export default generateCode
{t('labs.lab.code')} {t('labs.lab.type')} {t('labs.lab.requestedOn')} {t('labs.lab.status')}
{lab.code} {lab.type} {format(new Date(lab.requestedOn), 'yyyy-MM-dd hh:mm a')} {lab.status}