diff --git a/.tx/config b/.tx/config index d3ee9d90f0..cb49faa41c 100644 --- a/.tx/config +++ b/.tx/config @@ -37,15 +37,6 @@ replace_edited_strings = false keep_translations = false resource_name = esm-patient-allergies-app -[o:openmrs:p:openmrs-esm-patient-chart:r:esm-patient-appointments-app] -file_filter = packages/esm-patient-appointments-app/translations/.json -source_file = packages/esm-patient-appointments-app/translations/en.json -source_lang = en -type = KEYVALUEJSON -replace_edited_strings = false -keep_translations = false -resource_name = esm-patient-appointments-app - [o:openmrs:p:openmrs-esm-patient-chart:r:esm-patient-attachments-app] file_filter = packages/esm-patient-attachments-app/translations/.json source_file = packages/esm-patient-attachments-app/translations/en.json diff --git a/README.md b/README.md index ca8f1cbf6d..515679cdd4 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,6 @@ The `openmrs-esm-patient-chart` is a frontend module for the OpenMRS SPA. It contains various microfrontends that constitute widgets in a patient dashboard. These widgets include: - [Allergies](packages/esm-patient-allergies-app/README.md) -- [Appointments](packages/esm-patient-appointments-app/README.md) - [Attachments](packages/esm-patient-attachments-app/README.md) - [Biometrics](packages/esm-patient-biometrics-app/README.md) - [Conditions](packages/esm-patient-conditions-app/README.md) diff --git a/__mocks__/appointments.mock.ts b/__mocks__/appointments.mock.ts deleted file mode 100644 index 100c175c56..0000000000 --- a/__mocks__/appointments.mock.ts +++ /dev/null @@ -1,446 +0,0 @@ -export const mockAppointmentsData = { - data: [ - { - uuid: '0d54aa56-0411-47f9-9ce9-23d702965881', - appointmentNumber: '0000', - patient: { identifier: '100GEJ', name: 'John Wilson', uuid: '8673ee4f-e2ab-4077-ba55-4980f408773e' }, - service: { - appointmentServiceId: 1, - name: 'Outpatient', - description: null, - speciality: {}, - startTime: '', - endTime: '', - maxAppointmentsLimit: null, - durationMins: null, - location: {}, - uuid: 'e2ec9cf0-ec38-4d2b-af6c-59c82fa30b90', - color: '#006400', - initialAppointmentStatus: 'Scheduled', - creatorName: null, - }, - serviceType: null, - provider: null, - location: { name: 'Isolation Ward', uuid: '2131aff8-2e2a-480a-b7ab-4ac53250262b' }, - startDateTime: 1628598900000, - endDateTime: 1628599020000, - appointmentKind: 'WalkIn', - status: 'Scheduled', - comments: null, - additionalInfo: null, - providers: [], - recurring: false, - }, - { - uuid: '0f445395-74a7-4d8b-a6ae-8f5195cc172e', - appointmentNumber: '0000', - patient: { identifier: '100GEJ', name: 'John Wilson', uuid: '8673ee4f-e2ab-4077-ba55-4980f408773e' }, - service: { - appointmentServiceId: 1, - name: 'Outpatient', - description: null, - speciality: {}, - startTime: '', - endTime: '', - maxAppointmentsLimit: null, - durationMins: null, - location: {}, - uuid: 'e2ec9cf0-ec38-4d2b-af6c-59c82fa30b90', - color: '#006400', - initialAppointmentStatus: 'Scheduled', - creatorName: null, - }, - serviceType: null, - provider: null, - location: { name: 'Isolation Ward', uuid: '2131aff8-2e2a-480a-b7ab-4ac53250262b' }, - startDateTime: 1628598900000, - endDateTime: 1628599020000, - appointmentKind: 'WalkIn', - status: 'Scheduled', - comments: null, - additionalInfo: null, - providers: [], - recurring: false, - }, - { - uuid: 'c5f4fdab-2d48-4412-adf4-ee26a6b0816f', - appointmentNumber: '0000', - patient: { identifier: '100GEJ', name: 'John Wilson', uuid: '8673ee4f-e2ab-4077-ba55-4980f408773e' }, - service: { - appointmentServiceId: 1, - name: 'Outpatient', - description: null, - speciality: {}, - startTime: '', - endTime: '', - maxAppointmentsLimit: null, - durationMins: null, - location: {}, - uuid: 'e2ec9cf0-ec38-4d2b-af6c-59c82fa30b90', - color: '#006400', - initialAppointmentStatus: 'Scheduled', - creatorName: null, - }, - serviceType: null, - provider: null, - location: { name: 'Isolation Ward', uuid: '2131aff8-2e2a-480a-b7ab-4ac53250262b' }, - startDateTime: 1628599800000, - endDateTime: 1628600100000, - appointmentKind: 'Scheduled', - status: 'Scheduled', - comments: null, - additionalInfo: null, - providers: [], - recurring: false, - }, - { - uuid: '7cd38a6d-377e-491b-8284-b04cf8b8c6d8', - appointmentNumber: '0000', - patient: { identifier: '100GEJ', name: 'John Wilson', uuid: '8673ee4f-e2ab-4077-ba55-4980f408773e' }, - service: { - appointmentServiceId: 1, - name: 'Outpatient', - description: null, - speciality: {}, - startTime: '', - endTime: '', - maxAppointmentsLimit: null, - durationMins: null, - location: {}, - uuid: 'e2ec9cf0-ec38-4d2b-af6c-59c82fa30b90', - color: '#006400', - initialAppointmentStatus: 'Scheduled', - creatorName: null, - }, - serviceType: null, - provider: null, - location: { name: 'Isolation Ward', uuid: '2131aff8-2e2a-480a-b7ab-4ac53250262b' }, - startDateTime: 1630326900000, - endDateTime: 1630327200000, - appointmentKind: 'WalkIn', - status: 'Scheduled', - comments: 'Walk in appointments', - additionalInfo: null, - providers: [], - recurring: false, - }, - { - uuid: 'e10ce4e3-0e91-4b97-bc6c-9b5068e58428', - appointmentNumber: '0000', - patient: { identifier: '100GEJ', name: 'John Wilson', uuid: '8673ee4f-e2ab-4077-ba55-4980f408773e' }, - service: { - appointmentServiceId: 1, - name: 'Outpatient', - description: null, - speciality: {}, - startTime: '', - endTime: '', - maxAppointmentsLimit: null, - durationMins: null, - location: {}, - uuid: 'e2ec9cf0-ec38-4d2b-af6c-59c82fa30b90', - color: '#006400', - initialAppointmentStatus: 'Scheduled', - creatorName: null, - }, - serviceType: null, - provider: null, - location: { name: 'Isolation Ward', uuid: '2131aff8-2e2a-480a-b7ab-4ac53250262b' }, - startDateTime: 1631278200000, - endDateTime: 1631278560000, - appointmentKind: 'WalkIn', - status: 'Scheduled', - comments: 'Some additional notes', - additionalInfo: null, - providers: [], - recurring: false, - }, - { - uuid: '8671c6bf-4305-48e4-8d25-bdf227d5f7af', - appointmentNumber: '0000', - patient: { identifier: '100GEJ', name: 'John Wilson', uuid: '8673ee4f-e2ab-4077-ba55-4980f408773e' }, - service: { - appointmentServiceId: 1, - name: 'Outpatient', - description: null, - speciality: {}, - startTime: '', - endTime: '', - maxAppointmentsLimit: null, - durationMins: null, - location: {}, - uuid: 'e2ec9cf0-ec38-4d2b-af6c-59c82fa30b90', - color: '#006400', - initialAppointmentStatus: 'Scheduled', - creatorName: null, - }, - serviceType: null, - provider: null, - location: { name: 'Isolation Ward', uuid: '2131aff8-2e2a-480a-b7ab-4ac53250262b' }, - startDateTime: 1631367600000, - endDateTime: 1631368800000, - appointmentKind: 'WalkIn', - status: 'Scheduled', - comments: null, - additionalInfo: null, - providers: [], - recurring: false, - }, - { - uuid: 'cdb0676f-0805-4c3e-bfef-7757a005e892', - appointmentNumber: '0000', - patient: { identifier: '100GEJ', name: 'John Wilson', uuid: '8673ee4f-e2ab-4077-ba55-4980f408773e' }, - service: { - appointmentServiceId: 1, - name: 'Outpatient', - description: null, - speciality: {}, - startTime: '', - endTime: '', - maxAppointmentsLimit: null, - durationMins: null, - location: {}, - uuid: 'e2ec9cf0-ec38-4d2b-af6c-59c82fa30b90', - color: '#006400', - initialAppointmentStatus: 'Scheduled', - creatorName: null, - }, - serviceType: null, - provider: null, - location: { name: 'Isolation Ward', uuid: '2131aff8-2e2a-480a-b7ab-4ac53250262b' }, - startDateTime: 1631537400000, - endDateTime: 1631537760000, - appointmentKind: 'WalkIn', - status: 'Scheduled', - comments: 'Some additional notes', - additionalInfo: null, - providers: [], - recurring: false, - }, - { - uuid: '66565d8b-4849-4b7c-966a-554d6073f80c', - appointmentNumber: '0000', - patient: { identifier: '100GEJ', name: 'John Wilson', uuid: '8673ee4f-e2ab-4077-ba55-4980f408773e' }, - service: { - appointmentServiceId: 1, - name: 'Outpatient', - description: null, - speciality: {}, - startTime: '', - endTime: '', - maxAppointmentsLimit: null, - durationMins: null, - location: {}, - uuid: 'e2ec9cf0-ec38-4d2b-af6c-59c82fa30b90', - color: '#006400', - initialAppointmentStatus: 'Scheduled', - creatorName: null, - }, - serviceType: null, - provider: null, - location: { name: 'Isolation Ward', uuid: '2131aff8-2e2a-480a-b7ab-4ac53250262b' }, - startDateTime: 1631605800000, - endDateTime: 1631606100000, - appointmentKind: 'WalkIn', - status: 'Scheduled', - comments: 'Some additional notes', - additionalInfo: null, - providers: [], - recurring: false, - }, - { - uuid: '45dcc19d-dd14-4a07-95c6-afa264972a34', - appointmentNumber: '0000', - patient: { identifier: '100GEJ', name: 'John Wilson', uuid: '8673ee4f-e2ab-4077-ba55-4980f408773e' }, - service: { - appointmentServiceId: 1, - name: 'Outpatient', - description: null, - speciality: {}, - startTime: '', - endTime: '', - maxAppointmentsLimit: null, - durationMins: null, - location: {}, - uuid: 'e2ec9cf0-ec38-4d2b-af6c-59c82fa30b90', - color: '#006400', - initialAppointmentStatus: 'Scheduled', - creatorName: null, - }, - serviceType: { duration: 15, name: 'Chemotherapy', uuid: '53d58ff1-0c45-4e2e-9bd2-9cc826cb46e1' }, - provider: null, - location: { name: 'Isolation Ward', uuid: '2131aff8-2e2a-480a-b7ab-4ac53250262b' }, - startDateTime: 1631623800000, - endDateTime: 1631624160000, - appointmentKind: 'WalkIn', - status: 'Scheduled', - comments: 'Some additional notes', - additionalInfo: null, - providers: [], - recurring: false, - }, - { - uuid: 'fa4657ad-db46-487d-8e2e-a3858c906ae6', - appointmentNumber: '0000', - patient: { identifier: '100GEJ', name: 'John Wilson', uuid: '8673ee4f-e2ab-4077-ba55-4980f408773e' }, - service: { - appointmentServiceId: 1, - name: 'Outpatient', - description: null, - speciality: {}, - startTime: '', - endTime: '', - maxAppointmentsLimit: null, - durationMins: null, - location: {}, - uuid: 'e2ec9cf0-ec38-4d2b-af6c-59c82fa30b90', - color: '#006400', - initialAppointmentStatus: 'Scheduled', - creatorName: null, - }, - serviceType: { duration: 15, name: 'Chemotherapy', uuid: '53d58ff1-0c45-4e2e-9bd2-9cc826cb46e1' }, - provider: null, - location: { name: 'Isolation Ward', uuid: '2131aff8-2e2a-480a-b7ab-4ac53250262b' }, - startDateTime: 1631712720000, - endDateTime: 1631713080000, - appointmentKind: 'WalkIn', - status: 'Scheduled', - comments: 'Some value', - additionalInfo: null, - providers: [], - recurring: false, - }, - { - uuid: '45dcc19d-dd14-4a07-95c6-afa264972a35', - appointmentNumber: '0000', - patient: { identifier: '100GEJ', name: 'John Wilson', uuid: '8673ee4f-e2ab-4077-ba55-4980f408773e' }, - service: { - appointmentServiceId: 1, - name: 'Outpatient', - description: null, - speciality: {}, - startTime: '', - endTime: '', - maxAppointmentsLimit: null, - durationMins: null, - location: {}, - uuid: 'e2ec9cf0-ec38-4d2b-af6c-59c82fa30b90', - color: '#006400', - initialAppointmentStatus: 'Scheduled', - creatorName: null, - }, - serviceType: { duration: 15, name: 'Chemotherapy', uuid: '53d58ff1-0c45-4e2e-9bd2-9cc826cb46e1' }, - provider: null, - location: { name: 'Isolation Ward', uuid: '2131aff8-2e2a-480a-b7ab-4ac53250262b' }, - startDateTime: 1631623800000, - endDateTime: 1631624160000, - appointmentKind: 'WalkIn', - status: 'Scheduled', - comments: 'Some additional notes', - additionalInfo: null, - providers: [], - recurring: false, - }, - { - uuid: 'fa4657ad-db46-487d-8e2e-a3858c906ae7', - appointmentNumber: '0000', - patient: { identifier: '100GEJ', name: 'John Wilson', uuid: '8673ee4f-e2ab-4077-ba55-4980f408773e' }, - service: { - appointmentServiceId: 1, - name: 'Outpatient', - description: null, - speciality: {}, - startTime: '', - endTime: '', - maxAppointmentsLimit: null, - durationMins: null, - location: {}, - uuid: 'e2ec9cf0-ec38-4d2b-af6c-59c82fa30b90', - color: '#006400', - initialAppointmentStatus: 'Scheduled', - creatorName: null, - }, - serviceType: { duration: 15, name: 'Chemotherapy', uuid: '53d58ff1-0c45-4e2e-9bd2-9cc826cb46e1' }, - provider: null, - location: { name: 'Isolation Ward', uuid: '2131aff8-2e2a-480a-b7ab-4ac53250262b' }, - startDateTime: 1631712720000, - endDateTime: 1631713080000, - appointmentKind: 'WalkIn', - status: 'Scheduled', - comments: 'Some value', - additionalInfo: null, - providers: [], - recurring: false, - }, - ], -}; - -export const mockUseAppointmentServiceData = [ - { - appointmentServiceId: 1, - name: 'Outpatient', - description: null, - speciality: {}, - startDateTime: new Date().toISOString(), - endTime: '', - maxAppointmentsLimit: null, - durationMins: 15, - location: {}, - uuid: 'e2ec9cf0-ec38-4d2b-af6c-59c82fa30b90', - color: '#006400', - initialAppointmentStatus: 'Scheduled', - creatorName: null, - weeklyAvailability: [ - { - dayOfWeek: 'MONDAY', - startTime: '07:00:00', - endTime: '20:00:00', - maxAppointmentsLimit: null, - uuid: '7c7c53c8-c104-40cc-9926-50fc6fe4c4c1', - }, - { - dayOfWeek: 'TUESDAY', - startTime: '07:00:00', - endTime: '20:00:00', - maxAppointmentsLimit: null, - uuid: '7683b94e-6c48-4132-b402-54837a8c0fb2', - }, - { - dayOfWeek: 'SUNDAY', - startTime: '07:00:00', - endTime: '20:00:00', - maxAppointmentsLimit: null, - uuid: '00be8427-0037-4984-8875-6a5a2bc57e8e', - }, - { - dayOfWeek: 'FRIDAY', - startTime: '07:00:00', - endTime: '20:00:00', - maxAppointmentsLimit: null, - uuid: 'af6b8d5b-be05-4e24-8601-30573f848bec', - }, - { - dayOfWeek: 'THURSDAY', - startTime: '07:00:00', - endTime: '20:00:00', - maxAppointmentsLimit: null, - uuid: 'eb35e91b-6909-41fe-9d09-750b83fb3b9c', - }, - { - dayOfWeek: 'SATURDAY', - startTime: '07:00:00', - endTime: '20:00:00', - maxAppointmentsLimit: null, - uuid: '7f6347fd-c514-4fd2-ab79-d7fd760bf82f', - }, - { - dayOfWeek: 'WEDNESDAY', - startTime: '07:00:00', - endTime: '20:00:00', - maxAppointmentsLimit: null, - uuid: 'dad83f54-a0a2-4ba9-819b-01e906c89b69', - }, - ], - serviceTypes: [{ duration: 15, name: 'Chemotherapy', uuid: '53d58ff1-0c45-4e2e-9bd2-9cc826cb46e1' }], - }, -]; diff --git a/__mocks__/index.ts b/__mocks__/index.ts index be3ecee574..e9c23caa57 100644 --- a/__mocks__/index.ts +++ b/__mocks__/index.ts @@ -1,5 +1,4 @@ export * from './allergies.mock'; -export * from './appointments.mock'; export * from './chart-widgets-config.mock'; export * from './conditions.mock'; export * from './encounters.mock'; diff --git a/e2e/pages/attachments-page.ts b/e2e/pages/attachments-page.ts new file mode 100644 index 0000000000..8e40268c95 --- /dev/null +++ b/e2e/pages/attachments-page.ts @@ -0,0 +1,9 @@ +import { type Page } from '@playwright/test'; + +export class AttachmentsPage { + constructor(readonly page: Page) {} + + async goTo(uuid: string) { + await this.page.goto('/openmrs/spa/patient/' + uuid + '/chart/Attachments'); + } +} diff --git a/e2e/pages/immunizations-page.ts b/e2e/pages/immunizations-page.ts new file mode 100644 index 0000000000..850debaeb9 --- /dev/null +++ b/e2e/pages/immunizations-page.ts @@ -0,0 +1,10 @@ +import { type Page } from '@playwright/test'; + +export class ImmunizationsPage { + constructor(readonly page: Page) {} + readonly immunizationsTable = () => this.page.getByRole('table', { name: /immunizations summary/i }); + + async goTo(patientUuid: string) { + await this.page.goto(`/openmrs/spa/patient/${patientUuid}/chart/Immunizations`); + } +} diff --git a/e2e/pages/index.ts b/e2e/pages/index.ts index 45549ac2fa..77104f8ef5 100644 --- a/e2e/pages/index.ts +++ b/e2e/pages/index.ts @@ -1,7 +1,9 @@ export * from './allergies-page'; +export * from './attachments-page'; export * from './chart-page'; export * from './conditions-page'; +export * from './immunizations-page'; export * from './medications-page'; export * from './program-page'; -export * from './vitals-and-biometrics-page'; export * from './visits-page'; +export * from './vitals-and-biometrics-page'; diff --git a/e2e/specs/attachments.spec.ts b/e2e/specs/attachments.spec.ts new file mode 100644 index 0000000000..90885aec8d --- /dev/null +++ b/e2e/specs/attachments.spec.ts @@ -0,0 +1,91 @@ +import { expect } from '@playwright/test'; +import { generateRandomPatient, type Patient, deletePatient } from '../commands'; +import { test } from '../core'; +import { AttachmentsPage } from '../pages'; + +let patient: Patient; + +test.beforeEach(async ({ api }) => { + patient = await generateRandomPatient(api); +}); + +test('Add and remove an attachment', async ({ page }) => { + const attachmentsPage = new AttachmentsPage(page); + const filePath = './e2e/support/upload/brainScan.jpeg'; + + await test.step('When I go to the Attachments page', async () => { + await attachmentsPage.goTo(patient.uuid); + }); + + await test.step('And I click on the `Record attachments` link', async () => { + await page.getByRole('button', { name: /record attachments/i }).click(); + }); + + await test.step('Then I should see the `Add attachment` modal launch in the workspace', async () => { + await expect(page.getByText(/add attachment/i)).toBeVisible(); + }); + + await test.step('When I click on the `Upload files` tab', async () => { + await page.getByRole('tab', { name: /upload files/i }).click(); + }); + + await test.step('And I choose the attachment file to upload', async () => { + await page.on('filechooser', async (fileChooser) => { + await fileChooser.setFiles(filePath); + }); + await page.click('.cds--file-browse-btn'); + }); + + await test.step('And I add a description for the image to upload', async () => { + await page.getByLabel(/image description/i).clear(); + await page.getByLabel(/image description/i).fill('This is a brain scan image of the patient'); + }); + + await test.step('And I click on the `Add Attachment` button', async () => { + await page.getByRole('button', { name: /add Attachment/i }).click(); + }); + + await test.step('Then I should see a success toast notification', async () => { + await expect(page.getByText(/upload complete/i)).toBeVisible(); + }); + + await test.step('When I click on the `Close` button', async () => { + await page + .locator('button') + .filter({ hasText: /^Close$/ }) + .click(); + }); + + await test.step('Then I should see the file I uploaded displayed in the attachments table', async () => { + await expect(page.getByRole('button', { name: /brainScan.jpeg/i })).toBeVisible(); + }); + + await test.step('When I click on the `Table view` tab', async () => { + await page.getByLabel(/table view/i).click(); + }); + + await test.step('And I click the overflow menu in the table row of the uploaded file', async () => { + await page.getByRole('button', { name: /options/i }).click(); + }); + + await test.step('And I click on the `Delete` button', async () => { + await page.getByRole('menuitem', { name: /delete/i }).click(); + await page.getByRole('button', { name: /delete/i }).click(); + }); + + await test.step('Then I should see a success toast notification', async () => { + await expect(page.getByText(/file deleted/i)).toBeVisible(); + }); + + await test.step('And I should not see the deleted attachment in the list', async () => { + await expect(page.getByRole('button', { name: /brainScan.jpeg/i })).not.toBeVisible(); + }); + + await test.step('And the attachments table should be empty', async () => { + await expect(page.getByText(/There are no attachments to display for this patient/i)).toBeVisible(); + }); +}); + +test.afterEach(async ({ api }) => { + await deletePatient(api, patient.uuid); +}); diff --git a/e2e/specs/biometrics.spec.ts b/e2e/specs/biometrics.spec.ts index a2c9640b10..903b8cf637 100644 --- a/e2e/specs/biometrics.spec.ts +++ b/e2e/specs/biometrics.spec.ts @@ -12,25 +12,38 @@ test.beforeEach(async ({ api }) => { visit = await startVisit(api, patient.uuid); }); -test('Record biometrics', async ({ page, api }) => { +test('Record biometrics', async ({ page }) => { const biometricsPage = new BiometricsAndVitalsPage(page); await test.step('When I visit the vitals and biometrics page', async () => { await biometricsPage.goTo(patient.uuid); }); - await test.step('And I click the `Record biometrics` link to launch the form', async () => { + await test.step('And I click on the `Record biometrics` link to launch the form', async () => { await biometricsPage.page.getByText(/record biometrics/i).click(); }); - await test.step('And then I fill the form', async () => { + await test.step('Then I should see the `Record Vitals and Biometrics` form launch in the workspace', async () => { + await expect(biometricsPage.page.getByText(/record vitals and biometrics/i)).toBeVisible(); + }); + + await test.step('When I fill `170` as the height', async () => { await biometricsPage.page.getByRole('spinbutton', { name: /height/i }).fill('170'); + }); + + await test.step('And I fill `65` as the weight', async () => { await biometricsPage.page.getByRole('spinbutton', { name: /weight/i }).fill('65'); + }); + + await test.step('Then I should see `22.51` as the auto calculated body mass index', async () => { await expect(biometricsPage.page.getByRole('spinbutton', { name: /bmi/i })).toHaveValue('22.5'); + }); + + await test.step('When I fill `25` as the mid upper arm circumference ', async () => { await biometricsPage.page.getByRole('spinbutton', { name: /muac/i }).fill('25'); }); - await test.step('And I click the submit button', async () => { + await test.step('And I click on the `Save and close` button', async () => { await biometricsPage.page.getByRole('button', { name: /save and close/i }).click(); }); diff --git a/e2e/specs/clinical-forms.spec.ts b/e2e/specs/clinical-forms.spec.ts index 0c3342d1d3..ad2c843165 100644 --- a/e2e/specs/clinical-forms.spec.ts +++ b/e2e/specs/clinical-forms.spec.ts @@ -43,19 +43,31 @@ test('Fill a clinical form', async ({ page, api }) => { await expect(chartPage.page.getByRole('cell', { name: /surgical operation/i })).toBeVisible(); }); - await test.step('And if I fill a form', async () => { + await test.step('When I click the `Soap note template` link to launch the form', async () => { await chartPage.page.getByText(/soap note template/i).click(); + }); - await expect(chartPage.page.getByRole('button', { name: /save and close/i })).toBeVisible(); - await expect(chartPage.page.getByRole('button', { name: /discard/i })).toBeVisible(); + await test.step('Then I should see the `Soap note template` form launch in the workspace', async () => { + await expect(chartPage.page.getByText(/soap note template/i)).toBeVisible(); + }); + await test.step('When I fill the `Subjective findings` question', async () => { await chartPage.page.locator('#SOAPSubjectiveFindingsid').fill(subjectiveFindings); + }); + + await test.step('And I fill the `Objective findings` question', async () => { await chartPage.page.locator('#SOAPObjectiveFindingsid').fill(objectiveFindings); + }); + + await test.step('And I fill the `Assessment` question', async () => { await chartPage.page.locator('#SOAPAssessmentid').fill(assessment); + }); + + await test.step('And I fill the `Plan` question', async () => { await chartPage.page.locator('#SOAPPlanid').fill(plan); }); - await test.step('And I click the submit button', async () => { + await test.step('And I click on the `Save and close` button', async () => { await chartPage.page.getByRole('button', { name: /save and close/i }).click(); }); diff --git a/e2e/specs/conditions.spec.ts b/e2e/specs/conditions.spec.ts index 22f1fa3bb8..4fa38171c9 100644 --- a/e2e/specs/conditions.spec.ts +++ b/e2e/specs/conditions.spec.ts @@ -9,7 +9,7 @@ test.beforeEach(async ({ api }) => { patient = await generateRandomPatient(api); }); -test('Record, edit and delete a condition', async ({ page, api }) => { +test('Record, edit and delete a condition', async ({ page }) => { const conditionsPage = new ConditionsPage(page); const headerRow = conditionsPage.conditionsTable().locator('thead > tr'); const dataRow = conditionsPage.conditionsTable().locator('tbody > tr'); @@ -22,14 +22,28 @@ test('Record, edit and delete a condition', async ({ page, api }) => { await conditionsPage.page.getByText(/record conditions/i).click(); }); - await test.step('And I fill the form', async () => { + await test.step('Then I should see the conditions form launch in the workspace', async () => { + await expect(conditionsPage.page.getByText(/record a condition/i)).toBeVisible(); + }); + + await test.step('When I search for `Mental status change` in the search box', async () => { await page.getByPlaceholder(/search conditions/i).fill('mental'); + }); + + await test.step('And I select the condition', async () => { await page.getByRole('menuitem', { name: 'Mental status change' }).click(); + }); + + await test.step('And I set `10/07/2023` as the onset date', async () => { await page.getByLabel(/onset date/i).fill('10/07/2023'); await page.getByLabel(/onset date/i).press('Tab'); }); - await test.step('And I save the form', async () => { + await test.step('And I set the clinical status to `Active`', async () => { + await page.getByText('Active', { exact: true }).check(); + }); + + await test.step('And I click on the `Save & close` button', async () => { await page.getByRole('button', { name: /save & close/i }).click(); }); @@ -37,7 +51,7 @@ test('Record, edit and delete a condition', async ({ page, api }) => { await expect(conditionsPage.page.getByText(/condition saved/i)).toBeVisible(); }); - await test.step('And I should see the new condition added to the list', async () => { + await test.step('Then I should see the new condition added to the list', async () => { await expect(headerRow).toContainText(/condition/i); await expect(headerRow).toContainText(/date of onset/i); await expect(headerRow).toContainText(/status/i); @@ -46,20 +60,32 @@ test('Record, edit and delete a condition', async ({ page, api }) => { await expect(dataRow).toContainText(/active/i); }); - await test.step('Then if I click on the overflow menu and click the `Edit` button', async () => { - await expect(conditionsPage.page.getByRole('button', { name: /options/i })).toBeVisible(); - await conditionsPage.page.getByRole('button', { name: /options/i }).click(); - await expect(conditionsPage.page.getByRole('menu', { name: /edit or delete condition/i })).toBeVisible(); + await test.step('When I click the overflow menu in the table row with the newly created condition', async () => { + await conditionsPage.page + .getByRole('button', { name: /options/i }) + .nth(0) + .click(); + }); + + await test.step('And I click on the `Edit` button', async () => { await conditionsPage.page.getByRole('menuitem', { name: /edit/i }).click(); }); - await test.step('And I edit the condition', async () => { + await test.step('Then I should see the Conditions form launch in the workspace in edit mode`', async () => { + await expect(page.getByRole('cell', { name: /mental status change/i })).toBeVisible(); + }); + + await test.step('When I change the condition status to `Inactive`', async () => { await page.locator('label').filter({ hasText: 'Inactive' }).click(); - await page.getByLabel(/end date/i).fill('11/07/2023'); - await page.getByLabel(/end date/i).press('Tab'); }); - await test.step('And I save the form', async () => { + await test.step('And I change the on onset date to `11/07/2023`', async () => { + await page.getByLabel(/onset date/i).clear(); + await page.getByLabel(/onset date/i).fill('11/07/2023'); + await page.getByLabel(/onset date/i).press('Tab'); + }); + + await test.step('And I click on the `Save & close` button', async () => { await page.getByRole('button', { name: /save & close/i }).click(); }); @@ -76,16 +102,15 @@ test('Record, edit and delete a condition', async ({ page, api }) => { await expect(dataRow).toContainText(/inactive/i); }); - await test.step('And if I click the overflow menu and then click the `Delete` button', async () => { - await expect(conditionsPage.page.getByRole('button', { name: /options/i })).toBeVisible(); - await conditionsPage.page.getByRole('button', { name: /options/i }).click(); - await expect(conditionsPage.page.getByRole('menu', { name: /edit or delete condition/i })).toBeVisible(); - await conditionsPage.page.getByRole('menuitem', { name: /delete/i }).click(); - - await expect(conditionsPage.page.getByRole('heading', { name: /delete condition/i })).toBeVisible(); - await expect(conditionsPage.page.getByText(/are you sure you want to delete this condition/i)).toBeVisible(); - await expect(conditionsPage.page.getByRole('button', { name: /danger delete/i })).toBeVisible(); + await test.step('When I click the overflow menu in the table row with the updated condition', async () => { + await conditionsPage.page + .getByRole('button', { name: /options/i }) + .nth(0) + .click(); + }); + await test.step('And I click on the `Delete` button', async () => { + await conditionsPage.page.getByRole('menuitem', { name: /delete/i }).click(); await page.getByRole('button', { name: /danger delete/i }).click(); }); diff --git a/e2e/specs/drug-orders.spec.ts b/e2e/specs/drug-orders.spec.ts index f7fd7c76c3..9d19ceb7dc 100644 --- a/e2e/specs/drug-orders.spec.ts +++ b/e2e/specs/drug-orders.spec.ts @@ -46,7 +46,7 @@ test('Record, edit and discontinue a drug order', async ({ page }) => { .click(); }); - await test.step('Then I should be redirected to the drug order form', async () => { + await test.step('Then I should see the drug order form launch in the workspace', async () => { await expect(page.getByText(/order form/i)).toBeVisible(); }); @@ -84,7 +84,7 @@ test('Record, edit and discontinue a drug order', async ({ page }) => { await expect(page.getByText(/new/i)).toBeVisible(); }); - await test.step('When I click on the "Sign and close" button', async () => { + await test.step('When I click on the `Sign and close` button', async () => { await page.getByRole('button', { name: /sign and close/i }).click(); }); @@ -106,7 +106,7 @@ test('Record, edit and discontinue a drug order', async ({ page }) => { await expect(dataRow).toContainText(/indication headache/i); }); - await test.step('When I click the overflow menu of the created medication', async () => { + await test.step('When I click the overflow menu in the table row with the newly created medication', async () => { await page .getByRole('button', { name: /options/i }) .nth(0) @@ -117,7 +117,7 @@ test('Record, edit and discontinue a drug order', async ({ page }) => { await page.getByRole('menuitem', { name: /modify/i }).click(); }); - await test.step('Then I should be redirected to the order form of the created medication`', async () => { + await test.step('Then I should see the medication launch in the workspace in edit mode', async () => { await expect(page.getByText('Aspirin 81mg (81mg)')).toBeVisible(); }); @@ -146,7 +146,7 @@ test('Record, edit and discontinue a drug order', async ({ page }) => { await page.getByLabel(/indication/i).fill('Hypertension'); }); - await test.step('And I click on the "Save Order" button', async () => { + await test.step('And I click on the `Save Order` button', async () => { await page.getByRole('button', { name: /save order/i }).click(); }); @@ -155,7 +155,7 @@ test('Record, edit and discontinue a drug order', async ({ page }) => { await expect(page.getByText(/modify/i)).toBeVisible(); }); - await test.step('When I click on the "Sign and close" button', async () => { + await test.step('When I click on the `Sign and close` button', async () => { await page.getByRole('button', { name: /sign and close/i }).click(); }); @@ -182,7 +182,7 @@ test('Record, edit and discontinue a drug order', async ({ page }) => { await expect(dataRow.nth(0)).toContainText(/indication hypertension/i); }); - await test.step('When I click the overflow menu of the created medication', async () => { + await test.step('When I click the overflow menu in the table row with the updated medication', async () => { await page .getByRole('button', { name: /options/i }) .nth(0) @@ -197,7 +197,7 @@ test('Record, edit and discontinue a drug order', async ({ page }) => { await expect(page.getByText(/discontinue/i)).toBeVisible(); }); - await test.step('And I save the form', async () => { + await test.step('And I click on the `Sign and close` button', async () => { await page.getByRole('button', { name: /sign and close/i }).click(); }); diff --git a/e2e/specs/immunizations.spec.ts b/e2e/specs/immunizations.spec.ts new file mode 100644 index 0000000000..ae9fb53b1d --- /dev/null +++ b/e2e/specs/immunizations.spec.ts @@ -0,0 +1,62 @@ +import { expect } from '@playwright/test'; +import { type Visit } from '@openmrs/esm-framework'; +import { generateRandomPatient, type Patient, startVisit, deletePatient } from '../commands'; +import { test } from '../core'; +import { ImmunizationsPage } from '../pages'; + +let patient: Patient; +let visit: Visit; + +test.beforeEach(async ({ api }) => { + patient = await generateRandomPatient(api); + visit = await startVisit(api, patient.uuid); +}); + +test('Add an immunization', async ({ page }) => { + const immunizationsPage = new ImmunizationsPage(page); + const headerRow = immunizationsPage.immunizationsTable().locator('thead > tr'); + const immunizationType = immunizationsPage.immunizationsTable().locator('tbody td:nth-child(2)'); + const vaccinationDate = immunizationsPage.immunizationsTable().locator('tbody td:nth-child(3)'); + + await test.step('When I go to the Immunizations page', async () => { + await immunizationsPage.goTo(patient.uuid); + }); + + await test.step('And I click on the `Record immunizations` link', async () => { + await page.getByRole('button', { name: /record immunizations/i }).click(); + }); + + await test.step('Then I should see the Immunization form launch in the workspace', async () => { + await expect(page.getByText(/immunization form/i)).toBeVisible(); + }); + + await test.step('When I set `08/03/2024` as the vaccination date', async () => { + await page.getByLabel(/vaccination date/i).clear(); + await page.getByLabel(/vaccination date/i).fill('08/03/2024'); + await page.getByLabel(/vaccination date/i).press('Tab'); + }); + + await test.step('And I set `Hepatitis B vaccination` as the immunization', async () => { + await page.getByRole('combobox', { name: /immunization/i }).click(); + await page.getByText(/hepatitis b vaccination/i).click(); + }); + + await test.step('And I click on the `Save` button', async () => { + await page.getByRole('button', { name: /save/i }).click(); + }); + + await test.step('Then I should see a success toast notification', async () => { + await expect(page.getByText(/vaccination saved successfully/i)).toBeVisible(); + }); + + await test.step('And I should see the newly recorded immunization in the list', async () => { + await expect(headerRow).toContainText(/vaccine/i); + await expect(headerRow).toContainText(/recent vaccination/i); + await expect(immunizationType).toContainText(/hepatitis b vaccination/i); + await expect(vaccinationDate).toContainText(/mar 8, 2024/i); + }); +}); + +test.afterEach(async ({ api }) => { + await deletePatient(api, patient.uuid); +}); diff --git a/e2e/specs/lab-orders.spec.ts b/e2e/specs/lab-orders.spec.ts index 7a888d7034..bc95b74332 100644 --- a/e2e/specs/lab-orders.spec.ts +++ b/e2e/specs/lab-orders.spec.ts @@ -68,7 +68,7 @@ test('Record, edit and discontinue a lab order', async ({ page }) => { ).toBeVisible(); }); - await test.step('When I launch the overflow menu of the created lab order', async () => { + await test.step('When I click the overflow menu in the table row with the newly created lab order', async () => { await page .getByRole('button', { name: /options/i }) .nth(0) @@ -99,7 +99,7 @@ test('Record, edit and discontinue a lab order', async ({ page }) => { ).toBeVisible(); }); - await test.step('When I launch the overflow menu of the created lab order', async () => { + await test.step('When I click the overflow menu in the table row with the updated lab order', async () => { await page .getByRole('button', { name: /options/i }) .nth(0) diff --git a/e2e/specs/program-enrollment.spec.ts b/e2e/specs/program-enrollment.spec.ts index 47938ca227..66404a5efb 100644 --- a/e2e/specs/program-enrollment.spec.ts +++ b/e2e/specs/program-enrollment.spec.ts @@ -9,7 +9,7 @@ test.beforeEach(async ({ api }) => { patient = await generateRandomPatient(api); }); -test('Add and edit a program enrollment', async ({ page, api }) => { +test('Add and edit a program enrollment', async ({ page }) => { const programsPage = new ProgramsPage(page); const headerRow = programsPage.programsTable().locator('thead > tr'); const dataRow = programsPage.programsTable().locator('tbody > tr'); @@ -19,26 +19,39 @@ test('Add and edit a program enrollment', async ({ page, api }) => { }); await test.step('And I click on the `Record program enrollment` link', async () => { - await programsPage.page.getByText(/record program enrollment/i).click(); + await page.getByText(/record program enrollment/i).click(); }); - await test.step('And I record a program enrollment', async () => { - await programsPage.page.locator('#program').selectOption('64f950e6-1b07-4ac0-8e7e-f3e148f3463f'); - await programsPage.page.locator('#enrollmentDateInput').fill('04/07/2023'); - await programsPage.page.locator('#completionDateInput').fill('05/07/2023'); - await programsPage.page.locator('#completionDateInput').press('Tab'); - await programsPage.page.locator('#location').selectOption('44c3efb0-2583-4c80-a79e-1f756a03c0a1'); + await test.step('Then I should see the `Record program enrollment` form launch in the workspace', async () => { + await expect(page.getByText('Record program enrollment', { exact: true })).toBeVisible(); }); - await test.step('And I click the submit button', async () => { - await programsPage.page.getByRole('button', { name: /save and close/i }).click(); + await test.step('When I select the program named `Hiv Care and Treatment`', async () => { + await page.locator('#program').selectOption('64f950e6-1b07-4ac0-8e7e-f3e148f3463f'); }); - await test.step('And I should see a success toast notification', async () => { - await expect(programsPage.page.getByText(/program enrollment saved/i)).toBeVisible(); + await test.step('And I set `04/07/2023` as the enrollment date', async () => { + await page.locator('#enrollmentDateInput').fill('04/07/2023'); }); - await test.step('Then I should see newly recorded program enrollment in the list', async () => { + await test.step('And I set `05/07/2023` as the completion date', async () => { + await page.locator('#completionDateInput').fill('05/07/2023'); + await page.locator('#completionDateInput').press('Tab'); + }); + + await test.step('And I select `Outpatient Clinic` as the enrollment location', async () => { + await page.locator('#location').selectOption('44c3efb0-2583-4c80-a79e-1f756a03c0a1'); + }); + + await test.step('And I click on the `Save and close` button', async () => { + await page.getByRole('button', { name: /save and close/i }).click(); + }); + + await test.step('Then I should see a success toast notification', async () => { + await expect(page.getByText(/program enrollment saved/i)).toBeVisible(); + }); + + await test.step('And I should see newly recorded program enrollment in the list', async () => { await expect(headerRow).toContainText(/active programs/i); await expect(headerRow).toContainText(/location/i); await expect(headerRow).toContainText(/date enrolled/i); @@ -49,31 +62,43 @@ test('Add and edit a program enrollment', async ({ page, api }) => { await expect(dataRow).toContainText(/outpatient clinic/i); }); - await test.step('When I click the `Edit` button', async () => { + await test.step('When I click on the `Edit` button of the created program', async () => { await programsPage.editProgramButton().click(); }); - await test.step('And I edit the program enrollment', async () => { - await programsPage.page.locator('#enrollmentDateInput').clear(); - await programsPage.page.locator('#enrollmentDateInput').fill('03/07/2023'); - await programsPage.page.locator('#completionDateInput').clear(); - await programsPage.page.locator('#completionDateInput').fill('04/07/2023'); - await programsPage.page.locator('#completionDateInput').press('Tab'); - await programsPage.page.locator('#location').selectOption('1ce1b7d4-c865-4178-82b0-5932e51503d6'); + await test.step('Then I should see the program launch in the workspace in edit mode`', async () => { + await expect(page.getByRole('cell', { name: /hiv care and treatment/i })).toBeVisible(); + }); + + await test.step('When I change the enrollment date to `03/07/2023`', async () => { + await page.locator('#enrollmentDateInput').clear(); + await page.locator('#enrollmentDateInput').fill('03/07/2023'); + }); + + await test.step('And I change the completion date to `04/07/2023`', async () => { + await page.locator('#completionDateInput').clear(); + await page.locator('#completionDateInput').fill('04/07/2023'); + await page.locator('#completionDateInput').press('Tab'); + }); + + await test.step('And I change the enrollment location to `Community Outreach`', async () => { + await page.locator('#location').selectOption('1ce1b7d4-c865-4178-82b0-5932e51503d6'); }); - await test.step('And I click the submit button', async () => { - await programsPage.page.getByRole('button', { name: /save and close/i }).click(); + await test.step('And I click on the `Save and close` button', async () => { + await page.getByRole('button', { name: /save and close/i }).click(); }); - await test.step('And I should see a success toast notification', async () => { - await expect(programsPage.page.getByText(/program enrollment updated/i)).toBeVisible(); + await test.step('Then I should see a success toast notification', async () => { + await expect(page.getByText(/program enrollment updated/i)).toBeVisible(); }); - await test.step('Then I should see the updated program enrollment in the list', async () => { + await test.step('And I should see the updated program enrollment in the list', async () => { await expect(dataRow).toContainText(/hiv care and treatment/i); await expect(dataRow).toContainText(/03-Jul-2023/i); + await expect(dataRow).not.toContainText(/completed on 05-Jul-2023/i); await expect(dataRow).toContainText(/completed on 04-Jul-2023/i); + await expect(dataRow).not.toContainText(/outpatient clinic/i); await expect(dataRow).toContainText(/community outreach/i); }); }); diff --git a/e2e/specs/record-allergy.spec.ts b/e2e/specs/record-allergy.spec.ts index b10bd6cce4..4198fb64c3 100644 --- a/e2e/specs/record-allergy.spec.ts +++ b/e2e/specs/record-allergy.spec.ts @@ -9,7 +9,7 @@ test.beforeEach(async ({ api }) => { patient = await generateRandomPatient(api); }); -test('Record an allergy', async ({ page, api }) => { +test('Record an allergy', async ({ page }) => { const allergiesPage = new PatientAllergiesPage(page); const headerRow = allergiesPage.allergiesTable().locator('thead > tr'); const dataRow = allergiesPage.allergiesTable().locator('tbody > tr'); @@ -18,19 +18,32 @@ test('Record an allergy', async ({ page, api }) => { await allergiesPage.goTo(patient.uuid); }); - await test.step('And I click the `Record allergy intolerance` link to launch the form', async () => { + await test.step('And I click on the `Record allergy intolerance` link to launch the form', async () => { await allergiesPage.page.getByText(/record allergy intolerance/i).click(); }); - await test.step('And I fill the form', async () => { + await test.step('Then I should see the record allergy form launch in the workspace', async () => { + await expect(page.getByText(/record a new allergy/i)).toBeVisible(); + }); + + await test.step('When I select `ACE inhibitors` as the allergy', async () => { await allergiesPage.page.getByPlaceholder(/select the allergen/i).click(); await allergiesPage.page.getByText(/ace inhibitors/i).click(); + }); + + await test.step('And I select `Mental status change` as the reaction', async () => { await allergiesPage.page.getByText(/mental status change/i).click(); + }); + + await test.step('And I select `Mild` as the severity', async () => { await allergiesPage.page.getByText(/mild/i).click(); + }); + + await test.step('And I write `Test comment` as a comment', async () => { await allergiesPage.page.locator('#comments').fill('Test comment'); }); - await test.step('And I click the submit button', async () => { + await test.step('And I click on the `Save and close` button', async () => { await allergiesPage.page.getByRole('button', { name: /save and close/i }).click(); }); @@ -44,7 +57,7 @@ test('Record an allergy', async ({ page, api }) => { await expect(headerRow).toContainText(/reaction/i); await expect(headerRow).toContainText(/onset date and comments/i); await expect(dataRow).toContainText(/ace inhibitors/i); - await expect(dataRow).toContainText(/MILD/i); + await expect(dataRow).toContainText(/mild/i); await expect(dataRow).toContainText(/mental status change/i); await expect(dataRow).toContainText(/test comment/i); }); diff --git a/e2e/specs/start-visit.spec.ts b/e2e/specs/visit.spec.ts similarity index 86% rename from e2e/specs/start-visit.spec.ts rename to e2e/specs/visit.spec.ts index ee09ef68af..fc1d715cbb 100644 --- a/e2e/specs/start-visit.spec.ts +++ b/e2e/specs/visit.spec.ts @@ -9,18 +9,18 @@ test.beforeEach(async ({ api }) => { patient = await generateRandomPatient(api); }); -test('Start a visit', async ({ page, api }) => { +test('Start and end a visit', async ({ page }) => { const chartPage = new ChartPage(page); await test.step('When I visit the chart summary page', async () => { await chartPage.goTo(patient.uuid); }); - await test.step('And I click the `Start a visit` button ', async () => { + await test.step('And I click on the `Start a visit` button ', async () => { await chartPage.page.getByRole('button', { name: /start a visit/i }).click(); }); - await test.step('Then I should see the `Start Visit` form in the workspace', async () => { + await test.step('Then I should see the `Start Visit` form launch in the workspace', async () => { await expect(chartPage.page.getByText(/visit start date and time/i)).toBeVisible(); await expect(chartPage.page.getByPlaceholder(/dd\/mm\/yyyy/i)).toBeVisible(); await expect(chartPage.page.getByPlaceholder(/hh\:mm/i)).toBeVisible(); @@ -40,7 +40,7 @@ test('Start a visit', async ({ page, api }) => { await chartPage.page.getByText(/opd visit/i).click(); }); - await test.step('And I click the `Start Visit` button', async () => { + await test.step('And I click on the `Start Visit` button', async () => { await chartPage.page .locator('form') .getByRole('button', { name: /start a visit/i }) @@ -55,7 +55,7 @@ test('Start a visit', async ({ page, api }) => { await expect(chartPage.page.getByLabel(/active visit/i)).toBeVisible(); }); - await test.step('When I click the `End Visit` button', async () => { + await test.step('When I click on the `End Visit` button', async () => { await chartPage.page.getByRole('button', { name: /end visit/i }).click(); }); @@ -63,7 +63,7 @@ test('Start a visit', async ({ page, api }) => { await expect(chartPage.page.getByText(/are you sure you want to end this active visit?/i)).toBeVisible(); }); - await test.step('When I click the `End Visit` button to confirm', async () => { + await test.step('When I click on the `End Visit` button to confirm', async () => { await chartPage.page.getByRole('button', { name: 'danger End Visit' }).click(); }); diff --git a/e2e/specs/vitals.spec.ts b/e2e/specs/vitals.spec.ts index 36e85eaa8f..8a3b572db5 100644 --- a/e2e/specs/vitals.spec.ts +++ b/e2e/specs/vitals.spec.ts @@ -12,7 +12,7 @@ test.beforeEach(async ({ api }) => { visit = await startVisit(api, patient.uuid); }); -test('Record vital signs', async ({ page, api }) => { +test('Record vital signs', async ({ page }) => { const vitalsPage = new BiometricsAndVitalsPage(page); const headerRow = vitalsPage.vitalsTable().locator('thead > tr'); const dataRow = vitalsPage.vitalsTable().locator('tbody > tr'); @@ -21,21 +21,47 @@ test('Record vital signs', async ({ page, api }) => { await vitalsPage.goTo(patient.uuid); }); - await test.step('And I click `Record biometrics` link to launch the form', async () => { + await test.step('And I click the `Record biometrics` link to launch the form', async () => { await vitalsPage.page.getByText(/record vital signs/i).click(); }); - await test.step('And then I fill the form', async () => { + await test.step('Then I should see the `Record Vitals and Biometrics` form launch in the workspace', async () => { + await expect(vitalsPage.page.getByText(/record vitals and biometrics/i)).toBeVisible(); + }); + + await test.step('When I fill `37` as the temperature', async () => { await vitalsPage.page.getByRole('spinbutton', { name: /temperature/i }).fill('37'); + }); + + await test.step('And I fill `120` as the systolic', async () => { await vitalsPage.page.getByRole('spinbutton', { name: /systolic/i }).fill('120'); + }); + + await test.step('And I fill `100` as the diastolic', async () => { await vitalsPage.page.getByRole('spinbutton', { name: /diastolic/i }).fill('100'); + }); + + await test.step('And I fill `37` as the pulse', async () => { await vitalsPage.page.getByRole('spinbutton', { name: /pulse/i }).fill('65'); + }); + + await test.step('And I fill `37` as the respiration rate', async () => { await vitalsPage.page.getByRole('spinbutton', { name: /respiration rate/i }).fill('16'); + }); + + await test.step('And I fill `37` as the oxygen saturation', async () => { + await vitalsPage.page.getByRole('spinbutton', { name: /oxygen saturation/i }).fill('98'); + }); + + await test.step('And I add `37` as the oxygen saturation', async () => { await vitalsPage.page.getByRole('spinbutton', { name: /oxygen saturation/i }).fill('98'); + }); + + await test.step('And I add additional notes', async () => { await vitalsPage.page.getByPlaceholder(/type any additional notes here/i).fill('Test notes'); }); - await test.step('And I click the submit button', async () => { + await test.step('And I click on the `Save and close` button', async () => { await vitalsPage.page.getByRole('button', { name: /save and close/i }).click(); }); diff --git a/e2e/support/upload/brainScan.jpeg b/e2e/support/upload/brainScan.jpeg new file mode 100644 index 0000000000..4a8175c02c Binary files /dev/null and b/e2e/support/upload/brainScan.jpeg differ diff --git a/packages/esm-form-entry-app/src/app/form-creation/form-creation.service.ts b/packages/esm-form-entry-app/src/app/form-creation/form-creation.service.ts index 7a0ecd55bc..78f1e092fa 100644 --- a/packages/esm-form-entry-app/src/app/form-creation/form-creation.service.ts +++ b/packages/esm-form-entry-app/src/app/form-creation/form-creation.service.ts @@ -288,6 +288,11 @@ export class FormCreationService { const question = form.searchNodeByQuestionId(questionId); question[0]?.control?.setValue(value); }); + + if (encounterDate && preFilledQuestions.hasOwnProperty('encDate') && preFilledQuestions['encDate'] !== null) { + encounterDate[0].question.resetValueOnDisable = false; + encounterDate[0]?.control?.disable(); + } } } diff --git a/packages/esm-form-entry-app/src/single-spa-props.ts b/packages/esm-form-entry-app/src/single-spa-props.ts index 12a6f52627..06ca40fa15 100644 --- a/packages/esm-form-entry-app/src/single-spa-props.ts +++ b/packages/esm-form-entry-app/src/single-spa-props.ts @@ -49,7 +49,6 @@ export type SingleSpaProps = AppProps & VisitProperties & EncounterProperties & PatientProperties & - UIBehavior & PreFilledQuestions & ApplicationStatus & { additionalProps?: any; diff --git a/packages/esm-patient-appointments-app/README.md b/packages/esm-patient-appointments-app/README.md deleted file mode 100644 index 5bc1f43118..0000000000 --- a/packages/esm-patient-appointments-app/README.md +++ /dev/null @@ -1,4 +0,0 @@ -# esm-patient-appointments-app - -The appointments widget. It provides a tabular overview of the appointments recorded for a patient as well as a form for recording appointments. - diff --git a/packages/esm-patient-appointments-app/jest.config.js b/packages/esm-patient-appointments-app/jest.config.js deleted file mode 100644 index 0352f6214c..0000000000 --- a/packages/esm-patient-appointments-app/jest.config.js +++ /dev/null @@ -1,3 +0,0 @@ -const rootConfig = require('../../jest.config.js'); - -module.exports = rootConfig; diff --git a/packages/esm-patient-appointments-app/package.json b/packages/esm-patient-appointments-app/package.json deleted file mode 100644 index 8349046cd3..0000000000 --- a/packages/esm-patient-appointments-app/package.json +++ /dev/null @@ -1,56 +0,0 @@ -{ - "name": "@openmrs/esm-patient-appointments-app", - "version": "7.1.0", - "license": "MPL-2.0", - "description": "Patient appointments microfrontend for the OpenMRS SPA", - "browser": "dist/openmrs-esm-patient-appointments-app.js", - "main": "src/index.ts", - "source": true, - "scripts": { - "start": "openmrs develop", - "serve": "webpack serve --mode=development", - "debug": "npm run serve", - "build": "webpack --mode production --color", - "analyze": "webpack --mode=production --env analyze=true", - "lint": "cross-env eslint src --ext tsx,ts --fix --max-warnings=0", - "test": "cross-env TZ=UTC jest --config jest.config.js --verbose false --passWithNoTests --color", - "test:watch": "cross-env TZ=UTC jest --watch --config jest.config.js --color", - "coverage": "yarn test --coverage", - "typescript": "tsc", - "extract-translations": "i18next 'src/**/*.component.tsx' 'src/index.ts' --config ../../tools/i18next-parser.config.js" - }, - "browserslist": [ - "extends browserslist-config-openmrs" - ], - "keywords": [ - "openmrs" - ], - "homepage": "https://github.com/openmrs/openmrs-esm-patient-chart#readme", - "publishConfig": { - "access": "public" - }, - "repository": { - "type": "git", - "url": "git+https://github.com/openmrs/openmrs-esm-patient-chart.git" - }, - "bugs": { - "url": "https://github.com/openmrs/openmrs-esm-patient-chart/issues" - }, - "dependencies": { - "lodash-es": "^4.17.21" - }, - "peerDependencies": { - "@openmrs/esm-framework": "5.x", - "@openmrs/esm-patient-common-lib": "7.x", - "dayjs": "1.x", - "react": "^18.2.0", - "react-i18next": "11.x", - "react-router-dom": "6.x", - "rxjs": "6.x", - "swr": "2.x" - }, - "devDependencies": { - "@openmrs/esm-patient-common-lib": "workspace:*", - "webpack": "^5.88.2" - } -} diff --git a/packages/esm-patient-appointments-app/src/appointments/appointments-action-menu.component.tsx b/packages/esm-patient-appointments-app/src/appointments/appointments-action-menu.component.tsx deleted file mode 100644 index b3b839ae25..0000000000 --- a/packages/esm-patient-appointments-app/src/appointments/appointments-action-menu.component.tsx +++ /dev/null @@ -1,57 +0,0 @@ -import React, { useCallback } from 'react'; -import { useTranslation } from 'react-i18next'; - -import { Layer, OverflowMenu, OverflowMenuItem } from '@carbon/react'; -import { launchPatientWorkspace } from '@openmrs/esm-patient-common-lib'; -import { showModal, useLayoutType } from '@openmrs/esm-framework'; -import type { Appointment } from '../types'; -import styles from './appointments-action-menu.scss'; - -interface appointmentsActionMenuProps { - appointment: Appointment; - patientUuid: string; -} - -export const AppointmentsActionMenu = ({ appointment, patientUuid }: appointmentsActionMenuProps) => { - const { t } = useTranslation(); - const isTablet = useLayoutType() === 'tablet'; - - const launchEditAppointmentForm = useCallback( - () => - launchPatientWorkspace('appointments-form-workspace', { - workspaceTitle: t('editAppointment', 'Edit an appointment'), - appointment, - context: 'editing', - }), - [appointment, t], - ); - - const launchCancelAppointmentDialog = () => { - const dispose = showModal('appointment-cancel-confirmation-dialog', { - closeCancelModal: () => dispose(), - appointmentUuid: appointment.uuid, - patientUuid, - }); - }; - - return ( - - - - - - - ); -}; diff --git a/packages/esm-patient-appointments-app/src/appointments/appointments-action-menu.scss b/packages/esm-patient-appointments-app/src/appointments/appointments-action-menu.scss deleted file mode 100644 index 2aac4db7c1..0000000000 --- a/packages/esm-patient-appointments-app/src/appointments/appointments-action-menu.scss +++ /dev/null @@ -1,7 +0,0 @@ -.layer { - height: 100%; -} - -.menuItem { - max-width: none; -} diff --git a/packages/esm-patient-appointments-app/src/appointments/appointments-base.component.tsx b/packages/esm-patient-appointments-app/src/appointments/appointments-base.component.tsx deleted file mode 100644 index 5f8d62b138..0000000000 --- a/packages/esm-patient-appointments-app/src/appointments/appointments-base.component.tsx +++ /dev/null @@ -1,157 +0,0 @@ -import React, { useState } from 'react'; -import { useTranslation } from 'react-i18next'; -import dayjs from 'dayjs'; -import { Button, DataTableSkeleton, ContentSwitcher, InlineLoading, Layer, Switch, Tile } from '@carbon/react'; -import { Add } from '@carbon/react/icons'; -import { useLayoutType } from '@openmrs/esm-framework'; -import { CardHeader, EmptyDataIllustration, ErrorState, launchPatientWorkspace } from '@openmrs/esm-patient-common-lib'; -import { useAppointments } from './appointments.resource'; -import AppointmentsTable from './appointments-table.component'; -import styles from './appointments-base.scss'; - -interface AppointmentsBaseProps { - basePath?: string; - patientUuid: string; -} - -enum AppointmentTypes { - UPCOMING = 0, - TODAY = 1, - PAST = 2, -} - -const AppointmentsBase: React.FC = ({ patientUuid }) => { - const { t } = useTranslation(); - const headerTitle = t('appointments', 'Appointments'); - const isTablet = useLayoutType() === 'tablet'; - - const [switchedView, setSwitchedView] = useState(false); - - const [contentSwitcherValue, setContentSwitcherValue] = useState(0); - const startDate = dayjs(new Date().toISOString()).subtract(6, 'month').toISOString(); - const { - data: appointmentsData, - isError, - isLoading, - isValidating, - } = useAppointments(patientUuid, startDate, new AbortController()); - - const launchAppointmentsForm = () => launchPatientWorkspace('appointments-form-workspace'); - - if (isLoading) return ; - if (isError) { - return ; - } - if (Object.keys(appointmentsData)?.length) { - return ( -
- - {isValidating ? ( - - - - ) : null} -
- { - setContentSwitcherValue(index); - setSwitchedView(true); - }} - > - - - - -
|
- -
-
- {(() => { - if (contentSwitcherValue === AppointmentTypes.UPCOMING) { - if (appointmentsData.upcomingAppointments?.length) { - return ( - - ); - } - return ( - - - -

- {t( - 'noUpcomingAppointmentsForPatient', - 'There are no upcoming appointments to display for this patient', - )} -

-
-
- ); - } - if (contentSwitcherValue === AppointmentTypes.TODAY) { - if (appointmentsData.todaysAppointments?.length) { - return ( - - ); - } - return ( - - - -

- {t( - 'noCurrentAppointments', - 'There are no appointments scheduled for today to display for this patient', - )} -

-
-
- ); - } - - if (contentSwitcherValue === AppointmentTypes.PAST) { - if (appointmentsData.pastAppointments?.length) { - return ( - - ); - } - return ( - - - -

- {t('noPastAppointments', 'There are no past appointments to display for this patient')} -

-
-
- ); - } - })()} -
- ); - } -}; - -export default AppointmentsBase; diff --git a/packages/esm-patient-appointments-app/src/appointments/appointments-base.scss b/packages/esm-patient-appointments-app/src/appointments/appointments-base.scss deleted file mode 100644 index 307c3a490c..0000000000 --- a/packages/esm-patient-appointments-app/src/appointments/appointments-base.scss +++ /dev/null @@ -1,83 +0,0 @@ -@use '@carbon/colors'; -@use '@carbon/styles/scss/spacing'; -@use '@carbon/styles/scss/type'; -@import '@openmrs/esm-styleguide/src/vars'; - -// TO DO Move this styles to style - guide -// https://github.com/openmrs/openmrs-esm-core/blob/master/packages/framework/esm-styleguide/src/_vars.scss -$color-blue-30 : #a6c8ff; -$color-blue-10: #edf5ff; - -.widgetCard { - border: 1px solid $ui-03; -} - -.productiveHeading01 { - @include type.type-style("heading-compact-01"); -} - -.contentSwitcherWrapper { - display: flex; - width: fit-content; - justify-content: flex-end; - align-items: center; - width: 60%; -} - -.contentSwitcherWrapper > div > button { - background-color: $ui-02; -} - -.contentSwitcherWrapper > div button:first-child { - border-top: 1px solid $color-blue-30; - border-bottom: 1px solid $color-blue-30; - border-left: 1px solid $color-blue-30; - border-right: none; - border-radius: spacing.$spacing-02 0 0px spacing.$spacing-02; -} - -.contentSwitcherWrapper > div button:last-child { - border-top: 1px solid $color-blue-30; - border-bottom: 1px solid $color-blue-30; - border-right: 1px solid $color-blue-30; - border-left: none; - border-radius: 0px spacing.$spacing-02 spacing.$spacing-02 0px; -} - -.contentSwitcherWrapper > div > button[aria-selected=true], -.contentSwitcherWrapper > div > button[aria-selected=true]:first-child { - background-color: $color-blue-10; - color: $color-blue-60-2; - border-color: $color-blue-60-2; - border-right: 1px solid $color-blue-60-2; -} - -.contentSwitcherWrapper > div > button[aria-selected=true], -.contentSwitcherWrapper > div > button[aria-selected=true]:last-child { - background-color: $color-blue-10; - color: $color-blue-60-2; - border-color: $color-blue-60-2; - border-left: 1px solid $color-blue-60-2; -} - -.contentSwitcherWrapper > div > button[aria-selected=true]:focus { - box-shadow: none; -} - -.divider { - width: 1px; - height: spacing.$spacing-05; - color: colors.$gray-20; - margin: 0 spacing.$spacing-05; -} - -.content { - @include type.type-style("heading-compact-01"); - color: $text-02; - margin-top: spacing.$spacing-05; - margin-bottom: spacing.$spacing-03; -} - -.tile { - text-align: center; -} diff --git a/packages/esm-patient-appointments-app/src/appointments/appointments-base.test.tsx b/packages/esm-patient-appointments-app/src/appointments/appointments-base.test.tsx deleted file mode 100644 index e8aff24abb..0000000000 --- a/packages/esm-patient-appointments-app/src/appointments/appointments-base.test.tsx +++ /dev/null @@ -1,112 +0,0 @@ -import React from 'react'; -import { screen } from '@testing-library/react'; -import userEvent from '@testing-library/user-event'; -import { openmrsFetch, usePagination } from '@openmrs/esm-framework'; -import { mockAppointmentsData } from '__mocks__'; -import { mockPatient, patientChartBasePath, renderWithSwr, waitForLoadingToFinish } from 'tools'; -import AppointmentsBase from './appointments-base.component'; - -const testProps = { - basePath: patientChartBasePath, - patientUuid: mockPatient.id, -}; - -const mockOpenmrsFetch = openmrsFetch as jest.Mock; -const mockUsePagination = usePagination as jest.Mock; - -jest.mock('@openmrs/esm-framework', () => { - const originalModule = jest.requireActual('@openmrs/esm-framework'); - - return { - ...originalModule, - usePagination: jest.fn().mockImplementation(() => ({ - currentPage: 1, - goTo: () => {}, - results: [], - })), - }; -}); - -describe('AppointmensOverview', () => { - it('renders an empty state if appointments data is unavailable', async () => { - mockOpenmrsFetch.mockReturnValueOnce({ data: [] }); - - renderAppointments(); - - await waitForLoadingToFinish(); - - expect(screen.getByRole('heading', { name: /appointments/i })).toBeInTheDocument(); - expect(screen.getByRole('button', { name: /add/i })).toBeInTheDocument(); - }); - - it('renders an error state if there was a problem fetching appointments data', async () => { - const user = userEvent.setup(); - - const error = { - message: 'Internal server error', - response: { - status: 500, - statusText: 'Internal server error', - }, - }; - - mockOpenmrsFetch.mockRejectedValueOnce(error); - - renderAppointments(); - - await waitForLoadingToFinish(); - - expect(screen.getByRole('heading', { name: /appointments/i })).toBeInTheDocument(); - expect( - screen.getByText( - 'Sorry, there was a problem displaying this information. You can try to reload this page, or contact the site administrator and quote the error code above.', - ), - ).toBeInTheDocument(); - }); - - it(`renders a tabular overview of the patient's appointment schedule if available`, async () => { - const user = userEvent.setup(); - - mockOpenmrsFetch.mockReturnValueOnce(mockAppointmentsData); - mockUsePagination.mockImplementation(() => ({ - currentPage: 1, - goTo: () => {}, - results: mockAppointmentsData.data, - })); - - renderAppointments(); - - await waitForLoadingToFinish(); - - expect(screen.getByRole('heading', { name: /appointments/i })).toBeInTheDocument(); - expect(screen.getByRole('button', { name: /add/i })).toBeInTheDocument(); - - const upcomingAppointmentsTab = screen.getByRole('tab', { name: /upcoming/i }); - const pastAppointmentsTab = screen.getByRole('tab', { name: /past/i }); - - expect(screen.getByRole('tablist')).toContainElement(upcomingAppointmentsTab); - expect(screen.getByRole('tablist')).toContainElement(pastAppointmentsTab); - expect(screen.getByTitle(/Empty data illustration/i)).toBeInTheDocument(); - expect(screen.getByText(/There are no upcoming appointments to display for this patient/i)).toBeInTheDocument(); - - await user.click(pastAppointmentsTab); - expect(screen.getByRole('table')).toBeInTheDocument(); - - const expectedColumnHeaders = [/date/, /location/, /service/]; - expectedColumnHeaders.forEach((header) => { - expect(screen.getByRole('columnheader', { name: new RegExp(header, 'i') })).toBeInTheDocument(); - }); - - expect(screen.getAllByRole('row').length).toEqual(13); - - const previousPageButton = screen.getByRole('button', { name: /previous page/i }); - const nextPageButton = screen.getByRole('button', { name: /next page/i }); - - expect(previousPageButton).toBeDisabled(); - expect(nextPageButton).not.toBeDisabled(); - }); -}); - -function renderAppointments() { - renderWithSwr(); -} diff --git a/packages/esm-patient-appointments-app/src/appointments/appointments-cancel-modal.component.tsx b/packages/esm-patient-appointments-app/src/appointments/appointments-cancel-modal.component.tsx deleted file mode 100644 index 6e99f04967..0000000000 --- a/packages/esm-patient-appointments-app/src/appointments/appointments-cancel-modal.component.tsx +++ /dev/null @@ -1,66 +0,0 @@ -import React, { useState } from 'react'; -import { useTranslation } from 'react-i18next'; -import { Button, ModalBody, ModalFooter, ModalHeader } from '@carbon/react'; -import { showSnackbar } from '@openmrs/esm-framework'; -import { cancelAppointment, useAppointments } from './appointments.resource'; - -interface CancelAppointmentModalProps { - closeCancelModal: () => void; - appointmentUuid: string; - patientUuid: string; -} - -const CancelAppointmentModal: React.FC = ({ - closeCancelModal, - appointmentUuid, - patientUuid, -}) => { - const { t } = useTranslation(); - const { mutate } = useAppointments(patientUuid, new Date().toUTCString(), new AbortController()); - const [isSubmitting, setIsSubmitting] = useState(false); - - const handleCancel = async () => { - setIsSubmitting(true); - - cancelAppointment('Cancelled', appointmentUuid) - .then(({ status }) => { - if (status === 200) { - mutate(); - closeCancelModal(); - showSnackbar({ - isLowContrast: true, - kind: 'success', - subtitle: t('appointmentCancelledSuccessfully', 'Appointment cancelled successfully'), - title: t('appointmentCancelled', 'Appointment cancelled'), - }); - } - }) - .catch((err) => { - showSnackbar({ - title: t('appointmentCancelError', 'Error cancelling appointment'), - kind: 'error', - isLowContrast: true, - subtitle: err?.message, - }); - }); - }; - - return ( -
- - -

{t('cancelAppointmentModalConfirmationText', 'Are you sure you want to cancel this appointment?')}

-
- - - - -
- ); -}; - -export default CancelAppointmentModal; diff --git a/packages/esm-patient-appointments-app/src/appointments/appointments-detailed-summary.component.tsx b/packages/esm-patient-appointments-app/src/appointments/appointments-detailed-summary.component.tsx deleted file mode 100644 index 6734b61f7a..0000000000 --- a/packages/esm-patient-appointments-app/src/appointments/appointments-detailed-summary.component.tsx +++ /dev/null @@ -1,12 +0,0 @@ -import React from 'react'; -import AppointmentBase from './appointments-base.component'; - -interface AppointmentsDetailedSummaryProps { - patientUuid: string; -} - -const AppointmentsDetailedSummary: React.FC = ({ patientUuid }) => { - return ; -}; - -export default AppointmentsDetailedSummary; diff --git a/packages/esm-patient-appointments-app/src/appointments/appointments-overview.component.tsx b/packages/esm-patient-appointments-app/src/appointments/appointments-overview.component.tsx deleted file mode 100644 index 30f7ed768e..0000000000 --- a/packages/esm-patient-appointments-app/src/appointments/appointments-overview.component.tsx +++ /dev/null @@ -1,13 +0,0 @@ -import React from 'react'; -import AppointmentsBase from './appointments-base.component'; - -interface AppointmentOverviewProps { - basePath: string; - patientUuid: string; -} - -const AppointmentsOverview: React.FC = ({ patientUuid }) => ( - -); - -export default AppointmentsOverview; diff --git a/packages/esm-patient-appointments-app/src/appointments/appointments-table.component.tsx b/packages/esm-patient-appointments-app/src/appointments/appointments-table.component.tsx deleted file mode 100644 index 243de1bb77..0000000000 --- a/packages/esm-patient-appointments-app/src/appointments/appointments-table.component.tsx +++ /dev/null @@ -1,129 +0,0 @@ -import React, { useEffect, useMemo } from 'react'; -import classNames from 'classnames'; -import dayjs from 'dayjs'; -const utc = require('dayjs/plugin/utc'); -dayjs.extend(utc); -import { useTranslation } from 'react-i18next'; -import { - DataTable, - type DataTableHeader, - Table, - TableCell, - TableContainer, - TableBody, - TableHead, - TableHeader, - TableRow, -} from '@carbon/react'; -import { PatientChartPagination } from '@openmrs/esm-patient-common-lib'; -import { formatDatetime, parseDate, useLayoutType, usePagination } from '@openmrs/esm-framework'; -import { type Appointment } from '../types'; -import { AppointmentsActionMenu } from './appointments-action-menu.component'; -import styles from './appointments-table.scss'; - -const pageSize = 10; - -interface AppointmentTableProps { - patientAppointments: Array; - switchedView: boolean; - setSwitchedView: (value: boolean) => void; - patientUuid: string; -} - -const AppointmentsTable: React.FC = ({ - patientAppointments, - patientUuid, - switchedView, - setSwitchedView, -}) => { - const { t } = useTranslation(); - const { results: paginatedAppointments, currentPage, goTo } = usePagination(patientAppointments, pageSize); - const isTablet = useLayoutType() === 'tablet'; - - useEffect(() => { - if (switchedView && currentPage !== 1) { - goTo(1); - } - }, [switchedView, goTo, currentPage]); - - const tableHeaders: Array = useMemo( - () => [ - { key: 'date', header: t('date', 'Date') }, - { key: 'location', header: t('location', 'Location') }, - { key: 'service', header: t('service', 'Service') }, - { key: 'status', header: t('status', 'Status') }, - { key: 'type', header: t('type', 'Type') }, - { key: 'notes', header: t('notes', 'Notes') }, - ], - [t], - ); - - const tableRows = useMemo( - () => - paginatedAppointments?.map((appointment) => { - return { - id: appointment.uuid, - date: formatDatetime(parseDate(appointment.startDateTime), { mode: 'wide' }), - location: appointment?.location?.name ? appointment?.location?.name : '——', - service: appointment.service.name, - status: appointment.status, - type: appointment.appointmentKind ? appointment.appointmentKind : '——', - notes: appointment.comments ? appointment.comments : '——', - }; - }), - [paginatedAppointments], - ); - - return ( -
- - {({ rows, headers, getHeaderProps, getTableProps }) => ( - - - - - {headers.map((header) => ( - - {header.header?.content ?? header.header} - - ))} - - - - - {rows.map((row, i) => ( - - {row.cells.map((cell) => ( - {cell.value?.content ?? cell.value} - ))} - - - - - ))} - -
-
- )} -
- { - setSwitchedView(false); - goTo(page); - }} - pageNumber={currentPage} - pageSize={pageSize} - /> -
- ); -}; - -export default AppointmentsTable; diff --git a/packages/esm-patient-appointments-app/src/appointments/appointments-table.scss b/packages/esm-patient-appointments-app/src/appointments/appointments-table.scss deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/packages/esm-patient-appointments-app/src/appointments/appointments.resource.ts b/packages/esm-patient-appointments-app/src/appointments/appointments.resource.ts deleted file mode 100644 index 79f45c0386..0000000000 --- a/packages/esm-patient-appointments-app/src/appointments/appointments.resource.ts +++ /dev/null @@ -1,71 +0,0 @@ -import dayjs from 'dayjs'; -import useSWR from 'swr'; -import { openmrsFetch, restBaseUrl } from '@openmrs/esm-framework'; -import { type AppointmentsFetchResponse } from '../types'; -import isToday from 'dayjs/plugin/isToday'; -dayjs.extend(isToday); - -const appointmentsSearchUrl = `${restBaseUrl}/appointments/search`; - -export function useAppointments(patientUuid: string, startDate: string, abortController: AbortController) { - /* - SWR isn't meant to make POST requests for data fetching. This is a consequence of the API only exposing this resource via POST. - This works but likely isn't recommended. - */ - const fetcher = () => - openmrsFetch(appointmentsSearchUrl, { - method: 'POST', - signal: abortController.signal, - headers: { - 'Content-Type': 'application/json', - }, - body: { - patientUuid: patientUuid, - startDate: startDate, - }, - }); - - const { data, error, isLoading, isValidating, mutate } = useSWR( - appointmentsSearchUrl, - fetcher, - ); - - const appointments = data?.data?.length ? data.data : null; - - const pastAppointments = appointments - ?.sort((a, b) => (b.startDateTime > a.startDateTime ? 1 : -1)) - ?.filter(({ status }) => status !== 'Cancelled') - ?.filter(({ startDateTime }) => - dayjs(new Date(startDateTime).toISOString()).isBefore(new Date().setHours(0, 0, 0, 0)), - ); - - const upcomingAppointments = appointments - ?.sort((a, b) => (a.startDateTime > b.startDateTime ? 1 : -1)) - ?.filter(({ status }) => status !== 'Cancelled') - ?.filter(({ startDateTime }) => dayjs(new Date(startDateTime).toISOString()).isAfter(new Date())); - - const todaysAppointments = appointments - ?.sort((a, b) => (a.startDateTime > b.startDateTime ? 1 : -1)) - ?.filter(({ status }) => status !== 'Cancelled') - ?.filter(({ startDateTime }) => dayjs(new Date(startDateTime).toISOString()).isToday()); - - return { - data: data ? { pastAppointments, upcomingAppointments, todaysAppointments } : null, - isError: error, - isLoading, - isValidating, - mutate, - }; -} - -export const cancelAppointment = async (toStatus: string, appointmentUuid: string) => { - const omrsDateFormat = 'YYYY-MM-DDTHH:mm:ss.SSSZZ'; - const timeZone = Intl.DateTimeFormat().resolvedOptions().timeZone; - const statusChangeTime = dayjs(new Date()).format(omrsDateFormat); - const url = `${restBaseUrl}/appointments/${appointmentUuid}/status-change`; - return await openmrsFetch(url, { - body: { toStatus, onDate: statusChangeTime, timeZone: timeZone }, - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - }); -}; diff --git a/packages/esm-patient-appointments-app/src/appointments/upcoming-appointments-card.component.tsx b/packages/esm-patient-appointments-app/src/appointments/upcoming-appointments-card.component.tsx deleted file mode 100644 index 7b78ee6c84..0000000000 --- a/packages/esm-patient-appointments-app/src/appointments/upcoming-appointments-card.component.tsx +++ /dev/null @@ -1,105 +0,0 @@ -import React, { useEffect, useMemo } from 'react'; -import { useTranslation } from 'react-i18next'; -import isEmpty from 'lodash-es/isEmpty'; - -import { - Checkbox, - InlineLoading, - InlineNotification, - StructuredListHead, - StructuredListCell, - StructuredListRow, - StructuredListBody, - StructuredListWrapper, -} from '@carbon/react'; -import { formatDate, parseDate } from '@openmrs/esm-framework'; -import { useAppointments } from './appointments.resource'; -import { ErrorState } from '@openmrs/esm-patient-common-lib'; - -import styles from './upcoming-appointments-card.scss'; -import dayjs from 'dayjs'; -interface UpcomingAppointmentsProps { - patientUuid: string; - setUpcomingAppointment: (value: any) => void; -} - -const UpcomingAppointmentsCard: React.FC = ({ patientUuid, setUpcomingAppointment }) => { - const { t } = useTranslation(); - const startDate = dayjs(new Date().toISOString()).subtract(6, 'month').toISOString(); - const headerTitle = t('upcomingAppointments', 'Upcoming appointments'); - - const ac = useMemo(() => new AbortController(), []); - useEffect(() => () => ac.abort(), [ac]); - const { data: appointmentsData, isError, isLoading } = useAppointments(patientUuid, startDate, ac); - - const todaysAppointments = appointmentsData?.todaysAppointments?.length ? appointmentsData?.todaysAppointments : []; - const futureAppointments = appointmentsData?.upcomingAppointments?.length - ? appointmentsData?.upcomingAppointments - : []; - const appointments = todaysAppointments.concat(futureAppointments); - const upcomingAppointment = !isEmpty(appointments) - ? appointments?.filter(({ dateHonored }) => dateHonored === null) - : []; - if (isError) { - return ; - } - if (isLoading) { - - - ; - } - - if (upcomingAppointment?.length) { - const structuredListBodyRowGenerator = () => { - return upcomingAppointment.map((appointment, i) => ( - - {formatDate(parseDate(appointment.startDateTime), { mode: 'wide' })} - {appointment.service ? appointment.service.name : '——'} - - (e.target.checked ? setUpcomingAppointment(appointment) : '')} - /> - - - )); - }; - - return ( -
-
-

{headerTitle}

- - {t('appointmentToFulfill', 'Select appointment(s) to fulfill')} - {' '} -
- - - - - {t('date', 'Date')} - {t('appointmentType', 'Appointment type')} - {t('action', 'Action')} - - - {structuredListBodyRowGenerator()} - -
- ); - } - return ( - - ); -}; - -export default UpcomingAppointmentsCard; diff --git a/packages/esm-patient-appointments-app/src/appointments/upcoming-appointments-card.scss b/packages/esm-patient-appointments-app/src/appointments/upcoming-appointments-card.scss deleted file mode 100644 index b9b3c7bcb8..0000000000 --- a/packages/esm-patient-appointments-app/src/appointments/upcoming-appointments-card.scss +++ /dev/null @@ -1,47 +0,0 @@ -@use '@carbon/styles/scss/spacing'; -@use '@carbon/styles/scss/type'; -@import '@openmrs/esm-styleguide/src/vars'; - -.container { - margin: spacing.$spacing-05; - - & section { - margin: spacing.$spacing-05 0; - } -} - -.sectionTitle { - @include type.type-style("heading-compact-02"); - color: $text-02; - margin: 0 0 spacing.$spacing-03 0; -} -.checkbox { - &:not(:first-child) { - margin: 0rem 0rem; - } - } - - .input { - margin: 0rem 1rem 1rem; - } - - .headerLabel { - @include type.type-style('label-01'); - color: $text-02; - } - - .checkboxContainer { - display: grid; - grid-template-columns: 1fr 1fr; - } - - .structuredList { - padding: 0.5rem 0.5rem 0.5rem; - - } - - .inlineNotification { - width: 100%; - max-width: unset; - padding: '0rem'; - } diff --git a/packages/esm-patient-appointments-app/src/constants.ts b/packages/esm-patient-appointments-app/src/constants.ts deleted file mode 100644 index e4243ac16a..0000000000 --- a/packages/esm-patient-appointments-app/src/constants.ts +++ /dev/null @@ -1 +0,0 @@ -export const dateFormat = 'DD/MM/YYYY'; diff --git a/packages/esm-patient-appointments-app/src/dashboard.meta.ts b/packages/esm-patient-appointments-app/src/dashboard.meta.ts deleted file mode 100644 index fcd7822f17..0000000000 --- a/packages/esm-patient-appointments-app/src/dashboard.meta.ts +++ /dev/null @@ -1,6 +0,0 @@ -export const dashboardMeta = { - slot: 'patient-chart-appointments-dashboard-slot', - columns: 1, - title: 'Appointments', - path: 'Appointments', -}; diff --git a/packages/esm-patient-appointments-app/src/declarations.d.ts b/packages/esm-patient-appointments-app/src/declarations.d.ts deleted file mode 100644 index b2f3aa3b21..0000000000 --- a/packages/esm-patient-appointments-app/src/declarations.d.ts +++ /dev/null @@ -1,3 +0,0 @@ -declare module '*.css'; -declare module '*.scss'; -declare module '*.png'; diff --git a/packages/esm-patient-appointments-app/src/index.ts b/packages/esm-patient-appointments-app/src/index.ts deleted file mode 100644 index aa147fcbab..0000000000 --- a/packages/esm-patient-appointments-app/src/index.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { defineConfigSchema, getAsyncLifecycle, getSyncLifecycle, translateFrom } from '@openmrs/esm-framework'; -import { createDashboardLink, registerWorkspace } from '@openmrs/esm-patient-common-lib'; -import { dashboardMeta } from './dashboard.meta'; -import appointmentsOverviewComponent from './appointments/appointments-overview.component'; -import appointmentsDetailedSummaryComponent from './appointments/appointments-detailed-summary.component'; -import upcomingAppointmentsWidgetComponent from './appointments/upcoming-appointments-card.component'; - -export const importTranslation = require.context('../translations', false, /.json$/, 'lazy'); - -const moduleName = '@openmrs/esm-patient-appointments-app'; - -const options = { - featureName: 'patient-appointments', - moduleName, -}; - -export function startupApp() { - defineConfigSchema(moduleName, {}); -} - -export const appointmentsOverview = getSyncLifecycle(appointmentsOverviewComponent, options); - -export const appointmentsDetailedSummary = getSyncLifecycle(appointmentsDetailedSummaryComponent, options); - -// t('Appointments', 'Appointments') -export const appointmentsSummaryDashboardLink = getSyncLifecycle( - createDashboardLink({ ...dashboardMeta, moduleName }), - options, -); - -export const appointmentsCancelConfirmationDialog = getAsyncLifecycle( - () => import('./appointments/appointments-cancel-modal.component'), - options, -); - -export const upcomingAppointmentsWidget = getSyncLifecycle(upcomingAppointmentsWidgetComponent, options); diff --git a/packages/esm-patient-appointments-app/src/routes.json b/packages/esm-patient-appointments-app/src/routes.json deleted file mode 100644 index 02f9cce474..0000000000 --- a/packages/esm-patient-appointments-app/src/routes.json +++ /dev/null @@ -1,47 +0,0 @@ -{ - "$schema": "https://json.openmrs.org/routes.schema.json", - "backendDependencies": { - "webservices.rest": "^2.2.0" - }, - "extensions": [ - { - "name": "appointments-overview-widget", - "component": "appointmentsOverview", - "order": 8, - "meta": { - "columnSpan": 4 - } - }, - { - "name": "appointments-details-widget", - "component": "appointmentsDetailedSummary", - "slot": "patient-chart-appointments-dashboard-slot", - "meta": { - "columnSpan": 1 - } - }, - { - "name": "appointments-summary-dashboard", - "component": "appointmentsSummaryDashboardLink", - "slot": "patient-chart-dashboard-slot", - "order": 11, - "meta": { - "columns": 1, - "columnSpan": 1, - "slot": "patient-chart-appointments-dashboard-slot", - "title": "Appointments", - "path": "Appointments" - } - }, - { - "name": "appointment-cancel-confirmation-dialog", - "component": "appointmentsCancelConfirmationDialog" - }, - { - "name": "upcoming-appointment-widget", - "component": "upcomingAppointmentsWidget", - "slot": "upcoming-appointment-slot" - } - ], - "pages": [] -} diff --git a/packages/esm-patient-appointments-app/src/types/index.ts b/packages/esm-patient-appointments-app/src/types/index.ts deleted file mode 100644 index 13ad9918ee..0000000000 --- a/packages/esm-patient-appointments-app/src/types/index.ts +++ /dev/null @@ -1,45 +0,0 @@ -import type { OpenmrsResource } from '@openmrs/esm-framework'; - -export interface AppointmentsFetchResponse { - data: Array; -} - -export interface Appointment { - appointmentKind: string; - appointmentNumber: string; - comments: string; - endDateTime: Date | number; - location: OpenmrsResource; - patient: fhir.Patient; - provider: OpenmrsResource; - providers: Array; - recurring: boolean; - service: AppointmentService; - startDateTime: string; - status: string; - dateHonored: Date | number; - uuid: string; -} - -export interface ServiceTypes { - duration: number; - name: string; - uuid: string; -} - -export interface AppointmentService { - appointmentServiceId: number; - color: string; - creatorName: string; - description: string; - durationMins: number; - endTime: string; - initialAppointmentStatus: string; - location: OpenmrsResource; - maxAppointmentsLimit: number | null; - name: string; - speciality: OpenmrsResource; - startTime: string; - uuid: string; - serviceTypes: Array; -} diff --git a/packages/esm-patient-appointments-app/translations/am.json b/packages/esm-patient-appointments-app/translations/am.json deleted file mode 100644 index b2488a10c6..0000000000 --- a/packages/esm-patient-appointments-app/translations/am.json +++ /dev/null @@ -1,32 +0,0 @@ -{ - "action": "Action", - "add": "Add", - "appointmentCancelError": "Error cancelling appointment", - "appointmentCancelled": "Appointment cancelled", - "appointmentCancelledSuccessfully": "Appointment cancelled successfully", - "appointments": "Appointments", - "Appointments": "Appointments", - "appointmentToFulfill": "Select appointment(s) to fulfill", - "appointmentType": "Appointment type", - "appointmentType_title": "Appointment Type", - "cancel": "Cancel", - "cancelAppointment": "Cancel appointment", - "cancelAppointmentModalConfirmationText": "Are you sure you want to cancel this appointment?", - "date": "Date", - "discard": "Discard", - "edit": "Edit", - "editAppointment": "Edit an appointment", - "location": "Location", - "noCurrentAppointments": "There are no appointments scheduled for today to display for this patient", - "noPastAppointments": "There are no past appointments to display for this patient", - "notes": "Notes", - "noUpcomingAppointments": "No upcoming appointments found", - "noUpcomingAppointmentsForPatient": "There are no upcoming appointments to display for this patient", - "past": "Past", - "service": "Service", - "status": "Status", - "today": "Today", - "type": "Type", - "upcoming": "Upcoming", - "upcomingAppointments": "Upcoming appointments" -} diff --git a/packages/esm-patient-appointments-app/translations/ar.json b/packages/esm-patient-appointments-app/translations/ar.json deleted file mode 100644 index f4cded43a2..0000000000 --- a/packages/esm-patient-appointments-app/translations/ar.json +++ /dev/null @@ -1,32 +0,0 @@ -{ - "action": "الإجراء", - "add": "أضف", - "appointmentCancelError": "خطأ في إلغاء الموعد", - "appointmentCancelled": "تم إلغاء الموعد", - "appointmentCancelledSuccessfully": "تم إلغاء الموعد بنجاح", - "appointments": "المواعيد", - "Appointments": "المواعيد", - "appointmentToFulfill": "حدد الموعد/المواعيد لتنفيذه", - "appointmentType": "نوع الموعد", - "appointmentType_title": "نوع الموعد", - "cancel": "إلغاء", - "cancelAppointment": "إلغاء الموعد", - "cancelAppointmentModalConfirmationText": "هل أنت متأكد أنك تريد إلغاء هذا الموعد؟", - "date": "التاريخ", - "discard": "تجاهل", - "edit": "تعديل", - "editAppointment": "تعديل الموعد", - "location": "الموقع", - "noCurrentAppointments": "لا توجد مواعيد مجدولة لهذا المريض اليوم", - "noPastAppointments": "لا توجد مواعيد سابقة لعرضها لهذا المريض", - "notes": "الملاحظات", - "noUpcomingAppointments": "لم يتم العثور على مواعيد قادمة", - "noUpcomingAppointmentsForPatient": "لا توجد مواعيد قادمة لعرضها لهذا المريض", - "past": "الماضي", - "service": "الخدمة", - "status": "الحالة", - "today": "اليوم", - "type": "النوع", - "upcoming": "القادمة", - "upcomingAppointments": "المواعيد القادمة" -} diff --git a/packages/esm-patient-appointments-app/translations/en.json b/packages/esm-patient-appointments-app/translations/en.json deleted file mode 100644 index b2488a10c6..0000000000 --- a/packages/esm-patient-appointments-app/translations/en.json +++ /dev/null @@ -1,32 +0,0 @@ -{ - "action": "Action", - "add": "Add", - "appointmentCancelError": "Error cancelling appointment", - "appointmentCancelled": "Appointment cancelled", - "appointmentCancelledSuccessfully": "Appointment cancelled successfully", - "appointments": "Appointments", - "Appointments": "Appointments", - "appointmentToFulfill": "Select appointment(s) to fulfill", - "appointmentType": "Appointment type", - "appointmentType_title": "Appointment Type", - "cancel": "Cancel", - "cancelAppointment": "Cancel appointment", - "cancelAppointmentModalConfirmationText": "Are you sure you want to cancel this appointment?", - "date": "Date", - "discard": "Discard", - "edit": "Edit", - "editAppointment": "Edit an appointment", - "location": "Location", - "noCurrentAppointments": "There are no appointments scheduled for today to display for this patient", - "noPastAppointments": "There are no past appointments to display for this patient", - "notes": "Notes", - "noUpcomingAppointments": "No upcoming appointments found", - "noUpcomingAppointmentsForPatient": "There are no upcoming appointments to display for this patient", - "past": "Past", - "service": "Service", - "status": "Status", - "today": "Today", - "type": "Type", - "upcoming": "Upcoming", - "upcomingAppointments": "Upcoming appointments" -} diff --git a/packages/esm-patient-appointments-app/translations/es.json b/packages/esm-patient-appointments-app/translations/es.json deleted file mode 100644 index a4e8573f26..0000000000 --- a/packages/esm-patient-appointments-app/translations/es.json +++ /dev/null @@ -1,32 +0,0 @@ -{ - "action": "Acción", - "add": "Añadir", - "appointmentCancelError": "Error al cancelar la cita", - "appointmentCancelled": "Cita cancelada", - "appointmentCancelledSuccessfully": "Cita cancelada con éxito", - "appointments": "Citas", - "Appointments": "Citas", - "appointmentToFulfill": "Seleccionar cita(s) para cumplir", - "appointmentType": "Tipo de cita", - "appointmentType_title": "Tipo de cita", - "cancel": "Cancelar", - "cancelAppointment": "Cancelar cita", - "cancelAppointmentModalConfirmationText": "¿Está seguro de que desea cancelar esta cita?", - "date": "Fecha", - "discard": "Descartar", - "edit": "Editar", - "editAppointment": "Editar una cita", - "location": "Ubicación", - "noCurrentAppointments": "No hay citas programadas que mostrar para hoy para este paciente", - "noPastAppointments": "No hay citas anteriores para mostrar para este paciente", - "notes": "Apuntes", - "noUpcomingAppointments": "No hay próximas citas para mostrar para este paciente", - "noUpcomingAppointmentsForPatient": "No hay próximas citas para mostrar para este paciente", - "past": "Anterior", - "service": "Servicio", - "status": "Estado", - "today": "Hoy", - "type": "Tipo", - "upcoming": "Próximo/a", - "upcomingAppointments": "Próximas citas" -} diff --git a/packages/esm-patient-appointments-app/translations/fr.json b/packages/esm-patient-appointments-app/translations/fr.json deleted file mode 100644 index 2d2ceda958..0000000000 --- a/packages/esm-patient-appointments-app/translations/fr.json +++ /dev/null @@ -1,32 +0,0 @@ -{ - "action": "Action", - "add": "Ajouter", - "appointmentCancelError": "Error cancelling appointment", - "appointmentCancelled": "Appointment cancelled", - "appointmentCancelledSuccessfully": "Appointment cancelled successfully", - "appointments": "Rendez-vous", - "Appointments": "Appointments", - "appointmentToFulfill": "Select appointment(s) to fulfill", - "appointmentType": "Type de rendez-vous", - "appointmentType_title": "Appointment Type", - "cancel": "Cancel", - "cancelAppointment": "Cancel appointment", - "cancelAppointmentModalConfirmationText": "Are you sure you want to cancel this appointment?", - "date": "Date", - "discard": "Abandonner", - "edit": "Edit", - "editAppointment": "Edit an appointment", - "location": "Emplacement", - "noCurrentAppointments": "Il n'y a pas de rendez-vous prévu aujourd'hui à afficher pour ce patient", - "noPastAppointments": "Il n'y a pas de rendez-vous passé à afficher pour ce patient", - "notes": "Notes", - "noUpcomingAppointments": "Il n'y a pas de rendez-vous à venir à afficher pour ce patient", - "noUpcomingAppointmentsForPatient": "There are no upcoming appointments to display for this patient", - "past": "Passé", - "service": "Service", - "status": "Statut", - "today": "Aujourd'hui", - "type": "Type", - "upcoming": "A venir", - "upcomingAppointments": "Upcoming appointments" -} diff --git a/packages/esm-patient-appointments-app/translations/he.json b/packages/esm-patient-appointments-app/translations/he.json deleted file mode 100644 index 12c1011f5c..0000000000 --- a/packages/esm-patient-appointments-app/translations/he.json +++ /dev/null @@ -1,32 +0,0 @@ -{ - "action": "פעולה", - "add": "הוסף", - "appointmentCancelError": "שגיאה בביטול תור", - "appointmentCancelled": "התור בוטל", - "appointmentCancelledSuccessfully": "התור בוטל בהצלחה", - "appointments": "תורים", - "Appointments": "תורים", - "appointmentToFulfill": "בחר תור(ים) למילוי", - "appointmentType": "סוג התור", - "appointmentType_title": "סוג התור", - "cancel": "ביטול", - "cancelAppointment": "ביטול תור", - "cancelAppointmentModalConfirmationText": "?האם אתה בטוח שברצונך לבטל תור זה", - "date": "תאריך", - "discard": "בטל", - "edit": "ערוך", - "editAppointment": "ערוך תור", - "location": "מיקום", - "noCurrentAppointments": "אין תורים מתוזמנים להצגה עבור מטופל זה ליום הזה", - "noPastAppointments": "אין תורים עבריים להצגה עבור מטופל זה", - "notes": "הערות", - "noUpcomingAppointments": "אין תורים עתידיים להצגה עבור מטופל זה", - "noUpcomingAppointmentsForPatient": "אין תורים עתידיים להצגה עבור מטופל זה", - "past": "עבר", - "service": "שירות", - "status": "סטטוס", - "today": "היום", - "type": "סוג", - "upcoming": "עתידי", - "upcomingAppointments": "תורים עתידיים" -} diff --git a/packages/esm-patient-appointments-app/translations/km.json b/packages/esm-patient-appointments-app/translations/km.json deleted file mode 100644 index e7cf0ec160..0000000000 --- a/packages/esm-patient-appointments-app/translations/km.json +++ /dev/null @@ -1,32 +0,0 @@ -{ - "action": "សកម្មភាព", - "add": "បន្ថែម", - "appointmentCancelError": "មានបញ្ហាបច្ចេកទេសក្នុងការលុបចោលការណាត់ជួប", - "appointmentCancelled": "ការណាត់ជួបត្រូវបានលុបចោល", - "appointmentCancelledSuccessfully": "ការណាត់ជួបបានលុបចោលដោយជោគជ័យ", - "appointments": "ការណាត់ជួប", - "Appointments": "ការណាត់ជួប", - "appointmentToFulfill": "ជ្រើសរើសការណាត់ជួបដើម្បីបំពេញ", - "appointmentType": "ប្រភេទនៃការណាត់ជួប", - "appointmentType_title": "ប្រភេទនៃការណាត់ជួប", - "cancel": "បោះបង់", - "cancelAppointment": "លុបចោលការណាត់ជួប", - "cancelAppointmentModalConfirmationText": "តើអ្នកពិតជាចង់លុបចោលការណាត់ជួបនេះមែនទេ?", - "date": "កាលបរិច្ឆេទ", - "discard": "បោះបង់", - "edit": "កែសម្រួល", - "editAppointment": "កែសម្រួលការណាត់ជួប", - "location": "ទីតាំង", - "noCurrentAppointments": "មិនមានការណាត់ជួបសម្រាប់ថ្ងៃនេះ ដើម្បីបង្ហាញអ្នកជំងឺនេះទេ", - "noPastAppointments": "មិនមានការណាត់ជួបពីមុនដើម្បីបង្ហាញសម្រាប់អ្នកជំងឺនេះទេ", - "notes": "កំណត់ចំណាំ", - "noUpcomingAppointments": "មិនមានការណាត់ជួបនាពេលខាងមុខដើម្បីបង្ហាញសម្រាប់អ្នកជំងឺនេះទេ", - "noUpcomingAppointmentsForPatient": "មិនមានការណាត់ជួបនាពេលខាងមុខដើម្បីបង្ហាញសម្រាប់អ្នកជំងឺនេះទេ", - "past": "អតីតកាល", - "service": "សេវាកម្ម", - "status": "ស្ថានភាព", - "today": "ថ្ងៃនេះ", - "type": "ប្រភេទ", - "upcoming": "នាពេលខាងមុខ", - "upcomingAppointments": "ការណាត់ជួបនាពេលខាងមុខ" -} diff --git a/packages/esm-patient-appointments-app/translations/zh.json b/packages/esm-patient-appointments-app/translations/zh.json deleted file mode 100644 index 1050a3f05e..0000000000 --- a/packages/esm-patient-appointments-app/translations/zh.json +++ /dev/null @@ -1,32 +0,0 @@ -{ - "action": "操作", - "add": "添加", - "appointmentCancelError": "取消预约时出现错误", - "appointmentCancelled": "预约已取消", - "appointmentCancelledSuccessfully": "预约已成功取消", - "appointments": "预约", - "Appointments": "预约", - "appointmentToFulfill": "选择要履行的预约", - "appointmentType": "预约类型", - "appointmentType_title": "预约类型", - "cancel": "取消", - "cancelAppointment": "取消预约", - "cancelAppointmentModalConfirmationText": "您确定要取消这个预约吗?", - "date": "日期", - "discard": "放弃", - "edit": "编辑", - "editAppointment": "编辑一个预约", - "location": "地点", - "noCurrentAppointments": "该患者没有安排在今日的预约", - "noPastAppointments": "该患者没有历史预约", - "notes": "备注", - "noUpcomingAppointments": "未找到即将到来的预约", - "noUpcomingAppointmentsForPatient": "该患者没有即将到来的预约", - "past": "过往", - "service": "服务", - "status": "状态", - "today": "今日", - "type": "类型", - "upcoming": "即将到来", - "upcomingAppointments": "即将到来的预约" -} diff --git a/packages/esm-patient-appointments-app/translations/zh_CN.json b/packages/esm-patient-appointments-app/translations/zh_CN.json deleted file mode 100644 index 1050a3f05e..0000000000 --- a/packages/esm-patient-appointments-app/translations/zh_CN.json +++ /dev/null @@ -1,32 +0,0 @@ -{ - "action": "操作", - "add": "添加", - "appointmentCancelError": "取消预约时出现错误", - "appointmentCancelled": "预约已取消", - "appointmentCancelledSuccessfully": "预约已成功取消", - "appointments": "预约", - "Appointments": "预约", - "appointmentToFulfill": "选择要履行的预约", - "appointmentType": "预约类型", - "appointmentType_title": "预约类型", - "cancel": "取消", - "cancelAppointment": "取消预约", - "cancelAppointmentModalConfirmationText": "您确定要取消这个预约吗?", - "date": "日期", - "discard": "放弃", - "edit": "编辑", - "editAppointment": "编辑一个预约", - "location": "地点", - "noCurrentAppointments": "该患者没有安排在今日的预约", - "noPastAppointments": "该患者没有历史预约", - "notes": "备注", - "noUpcomingAppointments": "未找到即将到来的预约", - "noUpcomingAppointmentsForPatient": "该患者没有即将到来的预约", - "past": "过往", - "service": "服务", - "status": "状态", - "today": "今日", - "type": "类型", - "upcoming": "即将到来", - "upcomingAppointments": "即将到来的预约" -} diff --git a/packages/esm-patient-appointments-app/tsconfig.json b/packages/esm-patient-appointments-app/tsconfig.json deleted file mode 100644 index 29fd6726f2..0000000000 --- a/packages/esm-patient-appointments-app/tsconfig.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "extends": "../../tsconfig.json", - "include": ["src/**/*", "../../tools/setup-tests.ts"], -} diff --git a/packages/esm-patient-appointments-app/webpack.config.js b/packages/esm-patient-appointments-app/webpack.config.js deleted file mode 100644 index 2c74029c85..0000000000 --- a/packages/esm-patient-appointments-app/webpack.config.js +++ /dev/null @@ -1 +0,0 @@ -module.exports = require('openmrs/default-webpack-config'); diff --git a/packages/esm-patient-attachments-app/src/camera-media-uploader/media-uploader.component.tsx b/packages/esm-patient-attachments-app/src/camera-media-uploader/media-uploader.component.tsx index e5caf6195e..9b5e77f929 100644 --- a/packages/esm-patient-attachments-app/src/camera-media-uploader/media-uploader.component.tsx +++ b/packages/esm-patient-attachments-app/src/camera-media-uploader/media-uploader.component.tsx @@ -23,12 +23,25 @@ const MediaUploaderComponent = () => { if (file.size > maxFileSize * 1024 * 1024) { setErrorNotification({ title: t('fileSizeLimitExceededText', 'File size limit exceeded'), - subtitle: `${file.name} ${t('fileSizeLimitExceeded', 'exceeds the size limit of')} ${maxFileSize} MB.`, + subtitle: `The file "${file.name}" ${t( + 'fileSizeLimitExceeded', + 'exceeds the size limit of', + )} ${maxFileSize} MB.`, }); } else if (!isFileExtensionAllowed(file.name, allowedExtensions)) { + const lastExtension = allowedExtensions.pop(); + setErrorNotification({ - title: t('fileExtensionNotAllowedText', 'File extension is not allowed'), - subtitle: `${file.name} ${t('allowedExtensionsAre', 'Allowed extensions are: ')} ${allowedExtensions}.`, + title: t('unsupportedFileType', 'Unsupported file type'), + subtitle: t( + 'chooseAnAllowedFileType', + 'The file "{{fileName}}" cannot be uploaded. Please upload a file with one of the following extensions: {{supportedExtensions}}, or {{ lastExtension }}.', + { + fileName: file.name, + lastExtension: lastExtension, + supportedExtensions: allowedExtensions.join(', '), + }, + ), }); } else { // Convert MBs to bytes @@ -56,7 +69,14 @@ const MediaUploaderComponent = () => { if (!allowedExtensions) { return true; } - const fileExtension = fileName.split('.').pop(); + + const match = fileName.match(/\.[^.\s]+$/); + + if (!match) { + return false; + } + + const fileExtension = match[0].toLowerCase(); return allowedExtensions?.includes(fileExtension.toLowerCase()); }; @@ -81,7 +101,7 @@ const MediaUploaderComponent = () => {

'.' + ext) || ['*']} + accept={allowedExtensions?.map((ext) => ext) || ['*']} labelText={t('fileSizeInstructions', 'Drag and drop files here or click to upload')} tabIndex={0} multiple={multipleFiles} diff --git a/packages/esm-patient-attachments-app/translations/en.json b/packages/esm-patient-attachments-app/translations/en.json index 199c55a1fd..1266ff6a5c 100644 --- a/packages/esm-patient-attachments-app/translations/en.json +++ b/packages/esm-patient-attachments-app/translations/en.json @@ -4,7 +4,6 @@ "addAttachment_title": "Add Attachment", "addImage": "Add image +", "addMoreAttachments": "Add more attachments", - "allowedExtensionsAre": "Allowed extensions are:", "attachmentCaptionInstruction": "Enter caption", "attachments": "Attachments", "Attachments": "Attachments", @@ -14,6 +13,7 @@ "cameraError": "Camera Error", "cancel": "Cancel", "changeImage": "Change image", + "chooseAnAllowedFileType": "The file \"{{fileName}}\" cannot be uploaded. Please upload a file with one of the following extensions: {{supportedExtensions}}, or {{ lastExtension }}.", "closeModal": "Close", "closePreview": "Close preview", "dateUploaded": "Date uploaded", @@ -27,7 +27,6 @@ "fieldRequired": "This field is required", "file": "File", "fileDeleted": "File deleted", - "fileExtensionNotAllowedText": "File extension is not allowed", "fileName": "File name", "fileSizeInstructions": "Drag and drop files here or click to upload", "fileSizeLimitExceeded": "exceeds the size limit of", diff --git a/packages/esm-patient-chart-app/src/constants.ts b/packages/esm-patient-chart-app/src/constants.ts index bffc0fb82b..d01f53acb8 100644 --- a/packages/esm-patient-chart-app/src/constants.ts +++ b/packages/esm-patient-chart-app/src/constants.ts @@ -5,3 +5,4 @@ export const spaBasePath = `${window.spaBase}${basePath}`; export const moduleName = '@openmrs/esm-patient-chart-app'; export const patientChartWorkspaceSlot = 'patient-chart-workspace-slot'; export const patientChartWorkspaceHeaderSlot = 'patient-chart-workspace-header-slot'; +export const omrsDateFormat = 'YYYY-MM-DDTHH:mm:ss.SSSZZ'; diff --git a/packages/esm-patient-chart-app/src/visit/hooks/useUpcomingAppointments.tsx b/packages/esm-patient-chart-app/src/visit/hooks/useUpcomingAppointments.tsx index a878cbb253..4c8f8db0d9 100644 --- a/packages/esm-patient-chart-app/src/visit/hooks/useUpcomingAppointments.tsx +++ b/packages/esm-patient-chart-app/src/visit/hooks/useUpcomingAppointments.tsx @@ -1,5 +1,6 @@ import dayjs from 'dayjs'; -import { openmrsFetch, restBaseUrl, type OpenmrsResource } from '@openmrs/esm-framework'; +import { openmrsFetch, restBaseUrl, type OpenmrsResource, useAbortController } from '@openmrs/esm-framework'; +import { omrsDateFormat } from '../../constants'; export interface AppointmentPayload { patientUuid: string; @@ -17,13 +18,17 @@ export interface AppointmentPayload { dateHonored?: string; } -export function saveAppointment(appointment: AppointmentPayload, abortController: AbortController) { - return openmrsFetch(`${restBaseUrl}/appointment`, { +export const updateAppointmentStatus = async ( + toStatus: string, + appointmentUuid: string, + abortController: AbortController, +) => { + const statusChangeTime = dayjs().format(omrsDateFormat); + const url = `${restBaseUrl}/appointments/${appointmentUuid}/status-change`; + return await openmrsFetch(url, { + body: { toStatus, onDate: statusChangeTime }, method: 'POST', + headers: { 'Content-Type': 'application/json' }, signal: abortController.signal, - headers: { - 'Content-Type': 'application/json', - }, - body: appointment, }); -} +}; diff --git a/packages/esm-patient-chart-app/src/visit/visit-form/visit-form.component.tsx b/packages/esm-patient-chart-app/src/visit/visit-form/visit-form.component.tsx index 9318b04a3d..d698aa241f 100644 --- a/packages/esm-patient-chart-app/src/visit/visit-form/visit-form.component.tsx +++ b/packages/esm-patient-chart-app/src/visit/visit-form/visit-form.component.tsx @@ -44,7 +44,7 @@ import { import { MemoizedRecommendedVisitType } from './recommended-visit-type.component'; import { type ChartConfig } from '../../config-schema'; import { saveQueueEntry } from '../hooks/useServiceQueue'; -import { type AppointmentPayload, saveAppointment } from '../hooks/useUpcomingAppointments'; +import { updateAppointmentStatus } from '../hooks/useUpcomingAppointments'; import { useLocations } from '../hooks/useLocations'; import { useVisitQueueEntry } from '../queue-entry/queue.resource'; import BaseVisitType from './base-visit-type.component'; @@ -375,28 +375,16 @@ const StartVisitForm: React.FC = ({ ); } if (config.showUpcomingAppointments && upcomingAppointment) { - const appointmentPayload: AppointmentPayload = { - appointmentKind: upcomingAppointment?.appointmentKind, - serviceUuid: upcomingAppointment?.service.uuid, - startDateTime: upcomingAppointment?.startDateTime, - endDateTime: upcomingAppointment?.endDateTime, - locationUuid: visitLocation?.uuid, - patientUuid: patientUuid, - uuid: upcomingAppointment?.uuid, - dateHonored: dayjs(visitStartDate).format(), - }; - saveAppointment(appointmentPayload, abortController).then( - ({ status }) => { - if (status === 201) { - mutateCurrentVisit(); - mutateVisits(); - showSnackbar({ - isLowContrast: true, - kind: 'success', - subtitle: t('appointmentUpdate', 'Upcoming appointment updated successfully'), - title: t('appointmentEdited', 'Appointment edited'), - }); - } + updateAppointmentStatus('CheckedIn', upcomingAppointment?.uuid, abortController).then( + () => { + mutateCurrentVisit(); + mutateVisits(); + showSnackbar({ + isLowContrast: true, + kind: 'success', + subtitle: t('appointmentMarkedChecked', 'Appointment marked as Checked In'), + title: t('appointmentCheckedIn', 'Appointment Checked In'), + }); }, (error) => { showSnackbar({ diff --git a/packages/esm-patient-chart-app/translations/en.json b/packages/esm-patient-chart-app/translations/en.json index b713cdef52..7bde1db556 100644 --- a/packages/esm-patient-chart-app/translations/en.json +++ b/packages/esm-patient-chart-app/translations/en.json @@ -5,9 +5,9 @@ "all": "All", "allEncounters": "All encounters", "Allergies dashboard": "Allergies dashboard", - "appointmentEdited": "Appointment edited", + "appointmentCheckedIn": "Appointment checked in", + "appointmentMarkedChecked": "Appointment marked as Checked In", "Appointments dashboard": "Appointments dashboard", - "appointmentUpdate": "Upcoming appointment updated successfully", "Attachments dashboard": "Attachments dashboard", "cancel": "Cancel", "cancelActiveVisitConfirmation": "Are you sure you want to cancel this active visit?", diff --git a/packages/esm-patient-conditions-app/src/conditions/conditions-detailed-summary.component.tsx b/packages/esm-patient-conditions-app/src/conditions/conditions-detailed-summary.component.tsx index 4b1bdad552..80ff4e92a8 100644 --- a/packages/esm-patient-conditions-app/src/conditions/conditions-detailed-summary.component.tsx +++ b/packages/esm-patient-conditions-app/src/conditions/conditions-detailed-summary.component.tsx @@ -19,9 +19,9 @@ import { import { Add } from '@carbon/react/icons'; import { formatDate, parseDate, useLayoutType } from '@openmrs/esm-framework'; import { CardHeader, EmptyState, ErrorState, launchPatientWorkspace } from '@openmrs/esm-patient-common-lib'; -import { useConditions } from './conditions.resource'; import { ConditionsActionMenu } from './conditions-action-menu.component'; import { compare } from './utils'; +import { useConditions } from './conditions.resource'; import styles from './conditions-detailed-summary.scss'; function ConditionsDetailedSummary({ patient }) { @@ -71,6 +71,7 @@ function ConditionsDetailedSummary({ patient }) { ...condition, id: condition.id, condition: condition.display, + abatementDateTime: condition.abatementDateTime, onsetDateTime: { sortKey: condition.onsetDateTime ?? '', content: condition.onsetDateTime @@ -88,7 +89,13 @@ function ConditionsDetailedSummary({ patient }) { : compare(cellA.sortKey, cellB.sortKey); }; - const launchConditionsForm = useCallback(() => launchPatientWorkspace('conditions-form-workspace'), []); + const launchConditionsForm = useCallback( + () => + launchPatientWorkspace('conditions-form-workspace', { + formContext: 'creating', + }), + [], + ); const handleConditionStatusChange = ({ selectedItem }) => setFilter(selectedItem); diff --git a/packages/esm-patient-conditions-app/src/conditions/conditions-detailed-summary.test.tsx b/packages/esm-patient-conditions-app/src/conditions/conditions-detailed-summary.test.tsx index 125d75ad10..75ed0f1b71 100644 --- a/packages/esm-patient-conditions-app/src/conditions/conditions-detailed-summary.test.tsx +++ b/packages/esm-patient-conditions-app/src/conditions/conditions-detailed-summary.test.tsx @@ -92,7 +92,7 @@ it('clicking the Add button or Record Conditions link launches the conditions fo await user.click(recordConditionsLink); expect(launchPatientWorkspace).toHaveBeenCalledTimes(1); - expect(launchPatientWorkspace).toHaveBeenCalledWith('conditions-form-workspace'); + expect(launchPatientWorkspace).toHaveBeenCalledWith('conditions-form-workspace', { formContext: 'creating' }); }); function renderConditionsDetailedSummary() { diff --git a/packages/esm-patient-conditions-app/src/conditions/conditions-form.component.tsx b/packages/esm-patient-conditions-app/src/conditions/conditions-form.component.tsx index 88042796da..5192577943 100644 --- a/packages/esm-patient-conditions-app/src/conditions/conditions-form.component.tsx +++ b/packages/esm-patient-conditions-app/src/conditions/conditions-form.component.tsx @@ -15,14 +15,14 @@ interface ConditionFormProps extends DefaultWorkspaceProps { formContext: 'creating' | 'editing'; } -const conditionSchema = z.object({ +const schema = z.object({ + abatementDateTime: z.date().optional().nullable(), clinicalStatus: z.string(), - endDate: z.date().optional(), + conditionName: z.string({ required_error: 'A condition is required' }), onsetDateTime: z.date().nullable(), - search: z.string({ required_error: 'A condition is required' }), }); -export type ConditionFormData = z.infer; +export type ConditionSchema = z.infer; const ConditionsForm: React.FC = ({ closeWorkspace, @@ -33,52 +33,68 @@ const ConditionsForm: React.FC = ({ promptBeforeClosing, }) => { const { t } = useTranslation(); + const isTablet = useLayoutType() === 'tablet'; const { conditions } = useConditions(patientUuid); - const matchingCondition = conditions?.find((c) => c?.id === condition?.id); - const [isSubmittingForm, setIsSubmittingForm] = useState(false); const [errorCreating, setErrorCreating] = useState(null); const [errorUpdating, setErrorUpdating] = useState(null); + const matchingCondition = conditions?.find((c) => c?.id === condition?.id); - const methods = useForm({ + const methods = useForm({ mode: 'all', - resolver: zodResolver(conditionSchema), + resolver: zodResolver(schema), defaultValues: { + abatementDateTime: + formContext == 'editing' + ? matchingCondition?.abatementDateTime + ? new Date(matchingCondition?.abatementDateTime) + : null + : null, + conditionName: '', + clinicalStatus: condition?.cells?.find((cell) => cell?.info?.header === 'clinicalStatus')?.value ?? '', onsetDateTime: formContext == 'editing' ? matchingCondition?.onsetDateTime ? new Date(matchingCondition?.onsetDateTime) : null : null, - clinicalStatus: condition?.cells?.find((cell) => cell?.info?.header === 'clinicalStatus')?.value ?? 'Active', - search: '', }, }); const { setError, - formState: { isDirty, errors }, + formState: { isDirty }, } = methods; useEffect(() => { promptBeforeClosing(() => isDirty); }, [isDirty]); - const onSubmit: SubmitHandler = (data) => { + const onSubmit: SubmitHandler = (payload) => { setIsSubmittingForm(true); - if (formContext === 'creating' && !data.search.trim()) { - setError('search', { - type: 'manual', - message: t('conditionRequired', 'A condition is required'), - }); + + if (formContext === 'creating') { + if (!payload.conditionName.trim()) { + setError('conditionName', { + type: 'manual', + message: t('conditionRequired', 'A condition is required'), + }); + } + if (!payload.clinicalStatus) { + setError('clinicalStatus', { + type: 'manual', + message: t('clinicalStatusRequired', 'A clinical status is required'), + }); + } setIsSubmittingForm(false); - return; } + setIsSubmittingForm(true); }; - const onError = (error) => { + const onError = (e) => { + console.error('Error submitting condition: ', e); setIsSubmittingForm(false); }; @@ -86,13 +102,13 @@ const ConditionsForm: React.FC = ({
diff --git a/packages/esm-patient-conditions-app/src/conditions/conditions-form.scss b/packages/esm-patient-conditions-app/src/conditions/conditions-form.scss index 61e0273954..096955ee09 100644 --- a/packages/esm-patient-conditions-app/src/conditions/conditions-form.scss +++ b/packages/esm-patient-conditions-app/src/conditions/conditions-form.scss @@ -3,7 +3,16 @@ @import '@openmrs/esm-styleguide/src/vars'; .condition { - padding: 1rem 0.75rem; + padding: 0.75rem; + border-top: 0.0625rem solid $grey-2; + + &:first-of-type { + border-top: none; + } + + &:last-of-type { + border-bottom: none; + } } .conditionsList { @@ -66,7 +75,7 @@ .conditionsError { :global(.cds--search-input):focus { - outline: 2.5px solid $danger + outline: 2px solid $danger } :global(.cds--search-magnifier) { @@ -86,6 +95,10 @@ :global(.cds--radio-button-wrapper) { margin-block-end: .375rem !important; } + + :global(.cds--radio-button__validation-msg) { + display: none; + } } .spinner { diff --git a/packages/esm-patient-conditions-app/src/conditions/conditions-form.test.tsx b/packages/esm-patient-conditions-app/src/conditions/conditions-form.test.tsx index 5c1a94d231..52994ca66a 100644 --- a/packages/esm-patient-conditions-app/src/conditions/conditions-form.test.tsx +++ b/packages/esm-patient-conditions-app/src/conditions/conditions-form.test.tsx @@ -69,11 +69,11 @@ describe('Conditions Form', () => { expect(screen.getByRole('group', { name: /Condition/i })).toBeInTheDocument(); expect(screen.getByRole('textbox', { name: /Onset date/i })).toBeInTheDocument(); - expect(screen.getByRole('group', { name: /Current status/i })).toBeInTheDocument(); + expect(screen.getByRole('group', { name: /Clinical status/i })).toBeInTheDocument(); expect(screen.getByRole('searchbox', { name: /Enter condition/i })).toBeInTheDocument(); expect(screen.getByRole('button', { name: /Clear search input/i })).toBeInTheDocument(); expect(screen.getByRole('radio', { name: 'Active' })).toBeInTheDocument(); - expect(screen.getByRole('radio', { name: 'Active' })).toBeChecked(); + expect(screen.getByRole('radio', { name: 'Active' })).not.toBeChecked(); expect(screen.getByRole('radio', { name: 'Inactive' })).toBeInTheDocument(); expect(screen.getByRole('radio', { name: 'Inactive' })).not.toBeChecked(); @@ -86,55 +86,41 @@ describe('Conditions Form', () => { it('closes the form and the workspace when the cancel button is clicked', async () => { const user = userEvent.setup(); - renderConditionsForm(); const cancelButton = screen.getByRole('button', { name: /Cancel/i }); - await user.click(cancelButton); - expect(testProps.closeWorkspace).toHaveBeenCalledTimes(1); }); it('setting the status of a condition to "inactive" reveals an input for recording the end date', async () => { const user = userEvent.setup(); - renderConditionsForm(); expect(screen.getByText('Condition')).toBeInTheDocument(); - await user.click(screen.getByRole('radio', { name: /Inactive/i })); - expect(screen.getByLabelText(/End date/i)).toBeInTheDocument(); }); it('renders a list of related condition concepts when the user types in the searchbox', async () => { const user = userEvent.setup(); - renderConditionsForm(); const conditionSearchInput = screen.getByRole('searchbox', { name: /enter condition/i }); - expect(screen.queryByRole('menuitem', { name: /Headache/i })).not.toBeInTheDocument(); expect(screen.queryByDisplayValue('Headache')).not.toBeInTheDocument(); - await user.type(conditionSearchInput, 'Headache'); - expect(screen.getByDisplayValue(/headache/i)).toBeInTheDocument(); }); it('renders an error message when no matching conditions are found', async () => { const user = userEvent.setup(); - renderConditionsForm(); const conditionSearchInput = screen.getByRole('searchbox', { name: /enter condition/i }); - expect(screen.queryByRole('menuitem', { name: /Post-acute sequelae of COVID-19/i })).not.toBeInTheDocument(); expect(screen.queryByDisplayValue(/Post-acute sequelae of COVID-19/i)).not.toBeInTheDocument(); - await user.type(conditionSearchInput, 'Post-acute sequelae of COVID-19'); - expect(getByTextWithMarkup('No results for "Post-acute sequelae of COVID-19"')).toBeInTheDocument(); }); @@ -155,21 +141,16 @@ describe('Conditions Form', () => { const activeStatusInput = screen.getByRole('radio', { name: 'Active' }); const conditionSearchInput = screen.getByRole('searchbox', { name: /enter condition/i }); const onsetDateInput = screen.getByRole('textbox', { name: /onset date/i }); - expect(cancelButton).not.toBeDisabled(); - await user.type(conditionSearchInput, 'Headache'); await user.click(screen.getByRole('menuitem', { name: /headache/i })); + await user.click(activeStatusInput); await user.type(onsetDateInput, '2020-05-05'); - - expect(activeStatusInput).toBeChecked(); - await user.click(submitButton); }); it('renders an error notification if there was a problem recording a condition', async () => { const user = userEvent.setup(); - renderConditionsForm(); const submitButton = screen.getByRole('button', { name: /save & close/i }); @@ -186,15 +167,12 @@ describe('Conditions Form', () => { }; mockCreateCondition.mockImplementation(() => Promise.reject(error)); - await user.type(conditionSearchInput, 'Headache'); await user.click(screen.getByRole('menuitem', { name: /Headache/i })); await user.type(onsetDateInput, '2020-05-05'); await user.click(activeStatusInput); - expect(activeStatusInput).toBeChecked(); expect(submitButton).not.toBeDisabled(); - await user.click(submitButton); }); }); diff --git a/packages/esm-patient-conditions-app/src/conditions/conditions-overview.component.tsx b/packages/esm-patient-conditions-app/src/conditions/conditions-overview.component.tsx index 36dd5b4f00..1945d1f0ed 100644 --- a/packages/esm-patient-conditions-app/src/conditions/conditions-overview.component.tsx +++ b/packages/esm-patient-conditions-app/src/conditions/conditions-overview.component.tsx @@ -54,7 +54,13 @@ const ConditionsOverview: React.FC = ({ patientUuid }) const { conditions, isError, isLoading, isValidating } = useConditions(patientUuid); const [filter, setFilter] = useState<'All' | 'Active' | 'Inactive'>('Active'); - const launchConditionsForm = useCallback(() => launchPatientWorkspace('conditions-form-workspace'), []); + const launchConditionsForm = useCallback( + () => + launchPatientWorkspace('conditions-form-workspace', { + formContext: 'creating', + }), + [], + ); const filteredConditions = useMemo(() => { if (!filter || filter == 'All') { diff --git a/packages/esm-patient-conditions-app/src/conditions/conditions-overview.test.tsx b/packages/esm-patient-conditions-app/src/conditions/conditions-overview.test.tsx index f9340eb68a..e53160031e 100644 --- a/packages/esm-patient-conditions-app/src/conditions/conditions-overview.test.tsx +++ b/packages/esm-patient-conditions-app/src/conditions/conditions-overview.test.tsx @@ -117,7 +117,7 @@ describe('ConditionsOverview: ', () => { await user.click(recordConditionsLink); expect(launchPatientWorkspace).toHaveBeenCalledTimes(1); - expect(launchPatientWorkspace).toHaveBeenCalledWith('conditions-form-workspace'); + expect(launchPatientWorkspace).toHaveBeenCalledWith('conditions-form-workspace', { formContext: 'creating' }); }); }); diff --git a/packages/esm-patient-conditions-app/src/conditions/conditions-widget.component.tsx b/packages/esm-patient-conditions-app/src/conditions/conditions-widget.component.tsx index 059a229a74..18d6723e2a 100644 --- a/packages/esm-patient-conditions-app/src/conditions/conditions-widget.component.tsx +++ b/packages/esm-patient-conditions-app/src/conditions/conditions-widget.component.tsx @@ -1,5 +1,5 @@ import React, { type Dispatch, useCallback, useEffect, useRef, useState } from 'react'; -import { useTranslation } from 'react-i18next'; +import { type TFunction, useTranslation } from 'react-i18next'; import classNames from 'classnames'; import dayjs from 'dayjs'; import 'dayjs/plugin/utc'; @@ -23,48 +23,53 @@ import { type DefaultWorkspaceProps } from '@openmrs/esm-patient-common-lib'; import { type CodedCondition, type ConditionDataTableRow, - createCondition, type FormFields, + createCondition, updateCondition, useConditions, useConditionsSearch, } from './conditions.resource'; -import { type ConditionFormData } from './conditions-form.component'; +import { type ConditionSchema } from './conditions-form.component'; import styles from './conditions-form.scss'; interface ConditionsWidgetProps { closeWorkspaceWithSavedChanges?: DefaultWorkspaceProps['closeWorkspaceWithSavedChanges']; conditionToEdit?: ConditionDataTableRow; editing?: boolean; + isSubmittingForm: boolean; patientUuid: string; - setHasSubmissibleValue?: (value: boolean) => void; setErrorCreating?: (error: Error) => void; setErrorUpdating?: (error: Error) => void; - isSubmittingForm: boolean; + setHasSubmissibleValue?: (value: boolean) => void; setIsSubmittingForm: Dispatch; } +interface RequiredFieldLabelProps { + label: string; + t: TFunction; +} + const ConditionsWidget: React.FC = ({ closeWorkspaceWithSavedChanges, conditionToEdit, editing, - patientUuid, isSubmittingForm, - setIsSubmittingForm, + patientUuid, setErrorCreating, setErrorUpdating, + setIsSubmittingForm, }) => { const { t } = useTranslation(); const { conditions, mutate } = useConditions(patientUuid); const { control, - watch, - getValues, formState: { errors }, - } = useFormContext(); + getValues, + watch, + } = useFormContext(); const session = useSession(); const searchInputRef = useRef(null); - const currentStatus = watch('clinicalStatus'); + const clinicalStatus = watch('clinicalStatus'); const matchingCondition = conditions?.find((condition) => condition?.id === conditionToEdit?.id); const getFieldValue = ( @@ -79,10 +84,12 @@ const ConditionsWidget: React.FC = ({ const displayName = getFieldValue(conditionToEdit?.cells, 'display'); const editableClinicalStatus = getFieldValue(conditionToEdit?.cells, 'clinicalStatus'); + const editableAbatementDateTime = getFieldValue(conditionToEdit?.cells, 'abatementDateTime'); const [selectedCondition, setSelectedCondition] = useState(null); const [searchTerm, setSearchTerm] = useState(''); const debouncedSearchTerm = useDebounce(searchTerm); const { searchResults, isSearching } = useConditionsSearch(debouncedSearchTerm); + const handleConditionChange = useCallback((selectedCondition: CodedCondition) => { setSelectedCondition(selectedCondition); }, []); @@ -96,7 +103,7 @@ const ConditionsWidget: React.FC = ({ clinicalStatus: getValues('clinicalStatus'), conceptId: selectedCondition?.concept?.uuid, display: selectedCondition?.concept?.display, - endDate: getValues('endDate') ? dayjs(getValues('endDate')).format() : null, + abatementDateTime: getValues('abatementDateTime') ? dayjs(getValues('abatementDateTime')).format() : null, onsetDateTime: getValues('onsetDateTime') ? dayjs(getValues('onsetDateTime')).format() : null, patientId: patientUuid, userId: session?.user?.uuid, @@ -138,7 +145,11 @@ const ConditionsWidget: React.FC = ({ clinicalStatus: editing ? getValues('clinicalStatus') : editableClinicalStatus, conceptId: matchingCondition?.conceptId, display: displayName, - endDate: getValues('endDate') ? dayjs(getValues('endDate')).format() : null, + abatementDateTime: editing + ? getValues('abatementDateTime') + ? dayjs(getValues('abatementDateTime')).format() + : editableAbatementDateTime + : null, onsetDateTime: getValues('onsetDateTime') ? dayjs(getValues('onsetDateTime')).format() : null, patientId: patientUuid, userId: session?.user?.uuid, @@ -186,7 +197,7 @@ const ConditionsWidget: React.FC = ({ const handleSearchTermChange = (event: React.ChangeEvent) => setSearchTerm(event.target.value); useEffect(() => { - if (errors?.search) { + if (errors?.conditionName) { focusOnSearchInput(); } if (isSubmittingForm) { @@ -202,26 +213,15 @@ const ConditionsWidget: React.FC = ({ return (
- - {t('condition', 'Condition')} - {!editing && ( - - * - - )} - - } - > + }> {editing ? ( {displayName} ) : ( <> ( + render={({ field: { onChange, value } }) => ( = ({ labelText={t('enterCondition', 'Enter condition')} placeholder={t('searchConditions', 'Search conditions')} className={classNames({ - [styles.conditionsError]: errors?.search, + [styles.conditionsError]: errors?.conditionName, })} onChange={(e) => { onChange(e); handleSearchTermChange(e); }} - renderIcon={errors?.search && ((props) => )} - onBlur={onBlur} + renderIcon={errors?.conditionName && ((props) => )} onClear={() => { setSearchTerm(''); setSelectedCondition(null); @@ -256,7 +255,7 @@ const ConditionsWidget: React.FC = ({ )} /> - {errors?.search &&

{errors?.search?.message}

} + {errors?.conditionName &&

{errors?.conditionName?.message}

} {(() => { if (!debouncedSearchTerm || selectedCondition) return null; if (isSearching) @@ -264,12 +263,11 @@ const ConditionsWidget: React.FC = ({ if (searchResults && searchResults.length) { return (
    - {/*TODO: use uuid instead of index as the key*/} - {searchResults?.map((searchResult, index) => ( + {searchResults?.map((searchResult) => (
  • handleConditionChange(searchResult)} > {searchResult.display} @@ -313,51 +311,69 @@ const ConditionsWidget: React.FC = ({ )} /> - + }> ( - - + + )} /> + {errors?.clinicalStatus &&

    {errors?.clinicalStatus?.message}

    }
    - {currentStatus === 'inactive' && ( - ( - - onChange(date)} - onBlur={onBlur} - value={value} - > - - - - )} - /> + {(clinicalStatus.match(/inactive/i) || matchingCondition?.clinicalStatus?.match(/inactive/i)) && ( + + ( + <> + + onChange(date)} + onBlur={onBlur} + value={value} + > + + + + + )} + /> + )}
); }; +function RequiredFieldLabel({ label, t }: RequiredFieldLabelProps) { + return ( + <> + {label} + + + * + + + ); +} + export default ConditionsWidget; diff --git a/packages/esm-patient-conditions-app/src/conditions/conditions.resource.ts b/packages/esm-patient-conditions-app/src/conditions/conditions.resource.ts index 1ff5b26dd8..b04a2185e1 100644 --- a/packages/esm-patient-conditions-app/src/conditions/conditions.resource.ts +++ b/packages/esm-patient-conditions-app/src/conditions/conditions.resource.ts @@ -9,6 +9,7 @@ export type Condition = { onsetDateTime: string; recordedDate: string; id: string; + abatementDateTime?: string; }; export interface ConditionDataTableRow { @@ -51,7 +52,6 @@ type CreatePayload = { }, ]; }; - endDate: string; onsetDateTime: string; recorder: { reference: string; @@ -61,6 +61,7 @@ type CreatePayload = { subject: { reference: string; }; + abatementDateTime?: string; }; type EditPayload = CreatePayload & { @@ -71,7 +72,7 @@ export type FormFields = { clinicalStatus: string; conceptId: string; display: string; - endDate: string; + abatementDateTime: string; onsetDateTime: string; patientId: string; userId: string; @@ -79,7 +80,6 @@ export type FormFields = { export function useConditions(patientUuid: string) { const conditionsUrl = `${fhirBaseUrl}/Condition?patient=${patientUuid}&_count=100`; - const { data, error, isLoading, isValidating, mutate } = useSWR<{ data: FHIRConditionResponse }, Error>( patientUuid ? conditionsUrl : null, openmrsFetch, @@ -105,9 +105,7 @@ export function useConditions(patientUuid: string) { export function useConditionsSearch(conditionToLookup: string) { const config = useConfig(); const conditionConceptClassUuid = config?.conditionConceptClassUuid; - const conditionsSearchUrl = `${restBaseUrl}/conceptsearch?conceptClasses=${conditionConceptClassUuid}&q=${conditionToLookup}`; - const { data, error, isLoading } = useSWR<{ data: { results: Array } }, Error>( conditionToLookup ? conditionsSearchUrl : null, openmrsFetch, @@ -126,6 +124,7 @@ function mapConditionProperties(condition: FHIRCondition): Condition { clinicalStatus: status ? status.charAt(0).toUpperCase() + status.slice(1).toLowerCase() : '', conceptId: condition?.code?.coding[0]?.code, display: condition?.code?.coding[0]?.display, + abatementDateTime: condition?.abatementDateTime, onsetDateTime: condition?.onsetDateTime, recordedDate: condition?.recordedDate, id: condition?.id, @@ -153,7 +152,7 @@ export async function createCondition(payload: FormFields) { }, ], }, - endDate: payload.endDate, + abatementDateTime: payload.abatementDateTime, onsetDateTime: payload.onsetDateTime, recorder: { reference: `Practitioner/${payload.userId}`, @@ -198,7 +197,7 @@ export async function updateCondition(conditionId, payload: FormFields) { }, ], }, - endDate: payload.endDate, + abatementDateTime: payload.abatementDateTime, id: conditionId, onsetDateTime: payload.onsetDateTime, recorder: { diff --git a/packages/esm-patient-conditions-app/src/types/index.ts b/packages/esm-patient-conditions-app/src/types/index.ts index fbaf3e30ff..4b977aee4f 100644 --- a/packages/esm-patient-conditions-app/src/types/index.ts +++ b/packages/esm-patient-conditions-app/src/types/index.ts @@ -10,6 +10,7 @@ export interface FHIRConditionResponse { total: number; type: string; } + export interface FHIRCondition { clinicalStatus: { coding: Array; @@ -36,6 +37,7 @@ export interface FHIRCondition { div: string; status: string; }; + abatementDateTime?: string; } export interface CodingData { diff --git a/packages/esm-patient-conditions-app/translations/en.json b/packages/esm-patient-conditions-app/translations/en.json index 068734dc61..1f6dbeb2cd 100644 --- a/packages/esm-patient-conditions-app/translations/en.json +++ b/packages/esm-patient-conditions-app/translations/en.json @@ -1,16 +1,18 @@ { + "active": "Active", "add": "Add", "cancel": "Cancel", "checkFilters": "Check the filters above", + "clinicalStatus": "Clinical status", + "clinicalStatusRequired": "A clinical status is required", "condition": "Condition", - "conditionDeleted": "Condition Deleted", + "conditionDeleted": "Condition deleted", "conditionNowVisible": "It is now visible on the Conditions page", "conditionRequired": "A condition is required", "conditions": "Conditions", "Conditions": "Conditions", "conditionSaved": "Condition saved successfully", "conditionUpdated": "Condition updated", - "currentStatus": "Current status", "dateOfOnset": "Date of onset", "delete": "Delete", "deleteCondition": "Delete condition", @@ -23,6 +25,7 @@ "errorCreatingCondition": "Error creating condition", "errorDeletingCondition": "Error deleting condition", "errorUpdatingCondition": "Error updating condition", + "inactive": "Inactive", "noConditionsToDisplay": "No conditions to display", "noResultsFor": "No results for", "onsetDate": "Onset date", diff --git a/packages/esm-patient-immunizations-app/src/immunizations/immunizations-detailed-summary.component.tsx b/packages/esm-patient-immunizations-app/src/immunizations/immunizations-detailed-summary.component.tsx index 971df3b36a..356ca0ce39 100644 --- a/packages/esm-patient-immunizations-app/src/immunizations/immunizations-detailed-summary.component.tsx +++ b/packages/esm-patient-immunizations-app/src/immunizations/immunizations-detailed-summary.component.tsx @@ -144,7 +144,7 @@ const ImmunizationsDetailedSummary: React.FC {({ rows, headers, getHeaderProps, getRowProps, getTableProps, getExpandHeaderProps }) => ( - +
diff --git a/packages/esm-patient-notes-app/src/config-schema.ts b/packages/esm-patient-notes-app/src/config-schema.ts index 85dd5af32a..92819c6166 100644 --- a/packages/esm-patient-notes-app/src/config-schema.ts +++ b/packages/esm-patient-notes-app/src/config-schema.ts @@ -8,8 +8,15 @@ export const configSchema = { _description: 'The number of visits to load initially in the Visits Summary tab. Defaults to 5', _default: 5, }, + diagnosisConceptClass: { + _type: Type.UUID, + _description: 'The concept class to use for the diagnoses', + _default: '8d4918b0-c2cc-11de-8d13-0010c6dffd0f', + }, }; export interface ConfigObject { visitNoteConfig: VisitNoteConfigObject; + numberOfVisitsToLoad: number; + diagnosisConceptClass: string; } diff --git a/packages/esm-patient-notes-app/src/notes/visit-note-config-schema.ts b/packages/esm-patient-notes-app/src/notes/visit-note-config-schema.ts index 1310aad2b7..c40042d83c 100644 --- a/packages/esm-patient-notes-app/src/notes/visit-note-config-schema.ts +++ b/packages/esm-patient-notes-app/src/notes/visit-note-config-schema.ts @@ -28,4 +28,5 @@ export interface VisitNoteConfigObject { encounterNoteTextConceptUuid: string; encounterTypeUuid: string; formConceptUuid: string; + visitDiagnosesConceptUuid: string; } diff --git a/packages/esm-patient-notes-app/src/notes/visit-notes-form.component.tsx b/packages/esm-patient-notes-app/src/notes/visit-notes-form.component.tsx index 4a297f5dea..413b27e977 100644 --- a/packages/esm-patient-notes-app/src/notes/visit-notes-form.component.tsx +++ b/packages/esm-patient-notes-app/src/notes/visit-notes-form.component.tsx @@ -1,7 +1,8 @@ import React, { type SyntheticEvent, useCallback, useEffect, useMemo, useRef, useState } from 'react'; import dayjs from 'dayjs'; -import { useTranslation } from 'react-i18next'; import debounce from 'lodash-es/debounce'; +import { useTranslation, type TFunction } from 'react-i18next'; +import { mutate } from 'swr'; import { z } from 'zod'; import { zodResolver } from '@hookform/resolvers/zod'; import { useForm, Controller, type Control } from 'react-hook-form'; @@ -13,6 +14,7 @@ import { DatePickerInput, Form, FormGroup, + InlineNotification, Row, Search, SkeletonText, @@ -20,36 +22,34 @@ import { Tag, TextArea, Tile, - Layer, } from '@carbon/react'; import { Add, Edit, WarningFilled } from '@carbon/react/icons'; import { type UploadedFile, + createAttachment, createErrorHandler, ExtensionSlot, + ResponsiveWrapper, + restBaseUrl, showModal, showSnackbar, useConfig, useLayoutType, useSession, - createAttachment, - restBaseUrl, - ResponsiveWrapper, } from '@openmrs/esm-framework'; import { type DefaultWorkspaceProps } from '@openmrs/esm-patient-common-lib'; import type { ConfigObject } from '../config-schema'; import type { Concept, Diagnosis, DiagnosisPayload, VisitNotePayload } from '../types'; import { - fetchConceptDiagnosisByName, + fetchDiagnosisConceptsByName, savePatientDiagnosis, saveVisitNote, useInfiniteVisits, useVisitNotes, } from './visit-notes.resource'; import styles from './visit-notes-form.scss'; -import { mutate } from 'swr'; -const allowedImageTypes = ['jpeg', 'jpg', 'png', 'webp']; +const allowedImageTypes = ['.jpeg', '.jpg', '.png', '.webp']; const visitNoteFormSchema = z.object({ noteDate: z.date(), @@ -63,13 +63,25 @@ const visitNoteFormSchema = z.object({ type VisitNotesFormData = z.infer; +interface DiagnosesDisplayProps { + fieldName: string; + isDiagnosisNotSelected: (diagnosis: Concept) => boolean; + isLoading: boolean; + isSearching: boolean; + onAddDiagnosis: (diagnosis: Concept, searchInputField: string) => void; + searchResults: Array; + t: TFunction; + value: string; +} + interface DiagnosisSearchProps { - name: 'noteDate' | 'primaryDiagnosisSearch' | 'secondaryDiagnosisSearch' | 'clinicalNote'; - labelText: string; - placeholder: string; control: Control; - handleSearch: (fieldName) => void; error?: Object; + handleSearch: (fieldName) => void; + labelText: string; + name: 'noteDate' | 'primaryDiagnosisSearch' | 'secondaryDiagnosisSearch' | 'clinicalNote'; + placeholder: string; + setIsSearching: (isSearching: boolean) => void; } const VisitNotesForm: React.FC = ({ @@ -82,19 +94,21 @@ const VisitNotesForm: React.FC = ({ const { t } = useTranslation(); const isTablet = useLayoutType() === 'tablet'; const session = useSession(); - const config = useConfig() as ConfigObject; + const config = useConfig(); const state = useMemo(() => ({ patientUuid }), [patientUuid]); const { clinicianEncounterRole, encounterNoteTextConceptUuid, encounterTypeUuid, formConceptUuid } = config.visitNoteConfig; const [isHandlingSubmit, setIsHandlingSubmit] = useState(false); - const [loadingPrimary, setLoadingPrimary] = useState(false); - const [loadingSecondary, setLoadingSecondary] = useState(false); + const [isLoadingPrimaryDiagnoses, setIsLoadingPrimaryDiagnoses] = useState(false); + const [isLoadingSecondaryDiagnoses, setIsLoadingSecondaryDiagnoses] = useState(false); + const [isSearching, setIsSearching] = useState(false); const [selectedPrimaryDiagnoses, setSelectedPrimaryDiagnoses] = useState>([]); const [selectedSecondaryDiagnoses, setSelectedSecondaryDiagnoses] = useState>([]); - const [searchPrimaryResults, setSearchPrimaryResults] = useState>([]); - const [searchSecondaryResults, setSearchSecondaryResults] = useState>([]); + const [searchPrimaryResults, setSearchPrimaryResults] = useState>(null); + const [searchSecondaryResults, setSearchSecondaryResults] = useState>(null); const [combinedDiagnoses, setCombinedDiagnoses] = useState>([]); const [rows, setRows] = useState(); + const [error, setError] = useState(null); const { control, handleSubmit, watch, getValues, setValue, formState } = useForm({ mode: 'onSubmit', @@ -124,6 +138,7 @@ const VisitNotesForm: React.FC = ({ if (fieldQuery) { debouncedSearch(fieldQuery, fieldName); } + setIsSearching(false); }; const debouncedSearch = useMemo( @@ -131,25 +146,25 @@ const VisitNotesForm: React.FC = ({ debounce((fieldQuery, fieldName) => { if (fieldQuery) { if (fieldName === 'primaryDiagnosisSearch') { - setLoadingPrimary(true); + setIsLoadingPrimaryDiagnoses(true); } else if (fieldName === 'secondaryDiagnosisSearch') { - setLoadingSecondary(true); + setIsLoadingSecondaryDiagnoses(true); } - const sub = fetchConceptDiagnosisByName(fieldQuery).subscribe( - (matchingConceptDiagnoses: Array) => { + + fetchDiagnosisConceptsByName(fieldQuery, config.diagnosisConceptClass) + .then((matchingConceptDiagnoses: Array) => { if (fieldName == 'primaryDiagnosisSearch') { setSearchPrimaryResults(matchingConceptDiagnoses); - setLoadingPrimary(false); + setIsLoadingPrimaryDiagnoses(false); } else if (fieldName == 'secondaryDiagnosisSearch') { setSearchSecondaryResults(matchingConceptDiagnoses); - setLoadingSecondary(false); + setIsLoadingSecondaryDiagnoses(false); } - }, - () => createErrorHandler(), - ); - return () => { - sub.unsubscribe(); - }; + }) + .catch((e) => { + setError(e); + createErrorHandler(); + }); } }, searchTimeoutInMs), [], @@ -365,52 +380,48 @@ const VisitNotesForm: React.FC = ({ +
+ {selectedPrimaryDiagnoses && selectedPrimaryDiagnoses.length ? ( + <> + {selectedPrimaryDiagnoses.map((diagnosis, index) => ( + handleRemoveDiagnosis(diagnosis, 'primaryInputSearch')} + type="red" + > + {diagnosis.display} + + ))} + + ) : null} + {selectedSecondaryDiagnoses && selectedSecondaryDiagnoses.length ? ( + <> + {selectedSecondaryDiagnoses.map((diagnosis, index) => ( + handleRemoveDiagnosis(diagnosis, 'secondaryInputSearch')} + type="blue" + > + {diagnosis.display} + + ))} + + ) : null} + {selectedPrimaryDiagnoses && + !selectedPrimaryDiagnoses.length && + selectedSecondaryDiagnoses && + !selectedSecondaryDiagnoses.length && ( + {t('emptyDiagnosisText', 'No diagnosis selected — Enter a diagnosis below')} + )} +
{t('primaryDiagnosis', 'Primary diagnosis')} -
- {selectedPrimaryDiagnoses && selectedPrimaryDiagnoses.length ? ( - <> - {selectedPrimaryDiagnoses.map((diagnosis, index) => ( - handleRemoveDiagnosis(diagnosis, 'primaryInputSearch')} - style={{ marginRight: '0.5rem' }} - type={'red'} - > - {diagnosis.display} - - ))} - - ) : ( - <> - )} - {selectedSecondaryDiagnoses && selectedSecondaryDiagnoses.length ? ( - <> - {selectedSecondaryDiagnoses.map((diagnosis, index) => ( - handleRemoveDiagnosis(diagnosis, 'secondaryInputSearch')} - style={{ marginRight: '0.5rem' }} - type={'blue'} - > - {diagnosis.display} - - ))} - - ) : ( - <> - )} - {selectedPrimaryDiagnoses && - !selectedPrimaryDiagnoses.length && - selectedSecondaryDiagnoses && - !selectedSecondaryDiagnoses.length && ( - {t('emptyDiagnosisText', 'No diagnosis selected — Enter a diagnosis below')} - )} -
= ({ placeholder={t('primaryDiagnosisInputPlaceholder', 'Choose a primary diagnosis')} handleSearch={handleSearch} error={formState?.errors?.primaryDiagnosisSearch} + setIsSearching={setIsSearching} + /> + {error ? ( + setError(null)} + /> + ) : null} + -
- {(() => { - if (!getValues('primaryDiagnosisSearch')) return null; - if (loadingPrimary) - return ( - <> - - - - - - - ); - if (!loadingPrimary && searchPrimaryResults && searchPrimaryResults.length > 0) { - return ( -
    - {searchPrimaryResults.map((diagnosis, index) => { - if (isDiagnosisNotSelected(diagnosis)) { - return ( -
  • handleAddDiagnosis(diagnosis, 'primaryDiagnosisSearch')} - > - {diagnosis.display} -
  • - ); - } - })} -
- ); - } - return ( - <> - {isTablet ? ( - - - - {t('noMatchingDiagnoses', 'No diagnoses found matching')}{' '} - "{watch('primaryDiagnosisSearch')}" - - - - ) : ( - - - {t('noMatchingDiagnoses', 'No diagnoses found matching')}{' '} - "{watch('primaryDiagnosisSearch')}" - - - )} - - ); - })()} -
@@ -491,51 +466,27 @@ const VisitNotesForm: React.FC = ({ labelText={t('enterSecondaryDiagnoses', 'Enter Secondary diagnoses')} placeholder={t('secondaryDiagnosisInputPlaceholder', 'Choose a secondary diagnosis')} handleSearch={handleSearch} + setIsSearching={setIsSearching} + /> + {error ? ( + setError(null)} + /> + ) : null} + -
- {(() => { - if (!getValues('secondaryDiagnosisSearch')) return null; - if (loadingSecondary) - return ( - <> - - - - - - - ); - if (!loadingSecondary && searchSecondaryResults && searchSecondaryResults.length > 0) - return ( -
    - {searchSecondaryResults.map((diagnosis, index) => { - if (isDiagnosisNotSelected(diagnosis)) { - return ( -
  • handleAddDiagnosis(diagnosis, 'secondaryDiagnosisSearch')} - > - {diagnosis.display} -
  • - ); - } - })} -
- ); - return ( - - - - {t('noMatchingDiagnoses', 'No diagnoses found matching')}{' '} - "{watch('secondaryDiagnosisSearch')}" - - - - ); - })()} -
@@ -579,7 +530,7 @@ const VisitNotesForm: React.FC = ({

{currentImage?.base64Content ? ( ) : (