diff --git a/.env b/.env new file mode 100644 index 000000000..666a53f01 --- /dev/null +++ b/.env @@ -0,0 +1,6 @@ +# This is an environment file for configuring dynamic values. +E2E_BASE_URL=https://ohri-dev.globalhealthapp.net/openmrs +E2E_USER_ADMIN_USERNAME=admin +E2E_USER_ADMIN_PASSWORD=Admin123 +E2E_LOGIN_DEFAULT_LOCATION_UUID=44c3efb0-2583-4c80-a79e-1f756a03c0a1 +# The above location UUID is for the "Outpatient Clinic" location in the reference application diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml new file mode 100644 index 000000000..26ebd77d9 --- /dev/null +++ b/.github/workflows/e2e.yml @@ -0,0 +1,63 @@ +name: E2E Tests + +on: + push: + branches: + - main + pull_request: + branches: + - main + +jobs: + main: + runs-on: ubuntu-latest + steps: + - name: Checkout repo + uses: actions/checkout@v3 + + - name: Copy test environment variables + run: | + cp example.env .env + sed -i 's/8080/8180/g' .env + + - name: Setup node + uses: actions/setup-node@v3 + with: + node-version: 18 + + - name: Cache dependencies + id: cache + uses: actions/cache@v3 + with: + path: '**/node_modules' + key: ${{ runner.os }}-${{ hashFiles('**/yarn.lock') }} + + - name: Install dependencies + if: steps.cache.outputs.cache-hit != 'true' + run: yarn install --immutable + + - name: Install Playwright Browsers + run: npx playwright install chromium --with-deps + + - name: Run dev server + run: yarn start --sources 'packages/esm-*-app/' --port 8180 & # Refer to O3-1994 + + - name: Run E2E tests + run: yarn playwright test + + - name: Upload Report + uses: actions/upload-artifact@v3 + if: always() + with: + name: playwright-report + path: playwright-report/ + retention-days: 30 + + github-action: + runs-on: ubuntu-latest + steps: + - name: Checkout repo + uses: actions/checkout@v3 + + - name: Run Playwright tests with GitHub Action + run: yarn playwright test \ No newline at end of file diff --git a/.github/workflows/node.dev.js.yml b/.github/workflows/node.dev.js.yml deleted file mode 100644 index d04b86994..000000000 --- a/.github/workflows/node.dev.js.yml +++ /dev/null @@ -1,168 +0,0 @@ -name: OHRI CI - -on: - push: - branches: - - dev - pull_request: - branches: - - dev - release: - types: - - created - -jobs: - build: - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v4 - - name: Use Node.js - uses: actions/setup-node@v4 - with: - node-version: '18' - - - name: Cache dependencies - id: cache - uses: actions/cache@v3 - with: - path: '**/node_modules' - key: ${{ runner.os }}-${{ hashFiles('**/yarn.lock') }} - - - name: Install dependencies - if: steps.cache.outputs.cache-hit != 'true' - run: yarn install --immutable - - - name: Run tests - run: yarn run test - - # Temporarily disable typecheck - # - name: Run lint and typecheck - # run: yarn turbo run lint typescript --color --api="http://127.0.0.1:9080" --token="${{ secrets.TURBO_SERVER_TOKEN }}" --team="${{ github.repository_owner }}" - - - name: Run build - run: yarn turbo run build --color --concurrency=5 --api="http://127.0.0.1:9080" --token="${{ secrets.TURBO_SERVER_TOKEN }}" --team="${{ github.repository_owner }}" - - - name: Upload build artifacts - uses: actions/upload-artifact@v3 - with: - name: packages - path: | - packages/**/dist - - pre_release: - runs-on: ubuntu-latest - - needs: build - - if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/dev' }} - - steps: - - uses: actions/checkout@v4 - - name: Use Node.js - uses: actions/setup-node@v4 - with: - node-version: '18' - registry-url: 'https://registry.npmjs.org' - - - name: Cache dependencies - id: cache - uses: actions/cache@v3 - with: - path: | - **/node_modules - **/yarn.lock - - key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }} - restore-keys: | - ${{ runner.os }}-yarn- - - - name: Install dependencies - if: steps.cache.outputs.cache-hit != 'true' - run: yarn install --immutable - - - name: Version - run: yarn workspaces foreach --worktree --topological --exclude @ohri/openmrs-esm-ohri version "$(node -e "console.log(require('semver').inc(require('./package.json').version, 'patch'))")-pre.${{ github.run_number }}" - - - name: Build - run: yarn turbo run build --color --concurrency=5 - - - name: setup git config - run: | - git config user.name "GitHub Actions Bot" - git config user.email "<>" - - name: commit - run: | - git add . - git commit -m "Prerelease version" --no-verify - - - name: Pre-release - run: yarn config set npmAuthToken "${NODE_AUTH_TOKEN}" && yarn run ci:prepublish - env: - NODE_AUTH_TOKEN: ${{ secrets.NODE_AUTH_TOKEN }} - - - name: Upload Artifacts - uses: actions/upload-artifact@v3 - with: - name: packages - path: | - packages/**/dist - - docker_devserver_frontend: - runs-on: ubuntu-latest - needs: pre_release - - steps: - - name: Wait for 5 min - Let @esms reflect on NPM registry - run: sleep 300 - - name: Checkout repository - uses: actions/checkout@v3 - - name: Login to Docker Hub - uses: docker/login-action@v2 - with: - username: ${{ secrets.DOCKERHUB_USERNAME }} - password: ${{ secrets.DOCKERHUB_TOKEN }} - - name: Checkout - uses: actions/checkout@v3 - - name: Set up QEMU - uses: docker/setup-qemu-action@v2 - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v2 - - name: Build and push - uses: docker/build-push-action@v4 - with: - context: ./frontend - file: ./frontend/Dockerfile - platforms: linux/amd64,linux/arm64 - push: ${{ github.event_name != 'pull_request' }} - tags: ${{ secrets.DOCKERHUB_USERNAME }}/ohri-dev-frontend:next,${{ secrets.DOCKERHUB_USERNAME }}/ohri-dev-frontend:ci_${{ github.run_number }} - cache-from: type=inline,ref=user/app:buildcache - cache-to: type=inline,ref=user/app:buildcache,mode=max - - dev_deploy: - runs-on: ubuntu-latest - needs: docker_devserver_frontend - - if: (github.event_name == 'push' || github.event.pull_request.merged) - - steps: - - name: Checkout code - uses: actions/checkout@v2 - - - name: webfactory/ssh-agent - uses: webfactory/ssh-agent@v0.8.0 - - with: - ssh-private-key: ${{ secrets.OHRI_SSH_PRIVATE_KEY }} - - - name: SSH to remote OHRI Dev server via jump host - run: | - ssh -p 2216 \ - -o "UserKnownHostsFile=/dev/null" \ - -o "StrictHostKeyChecking=no" \ - -o "ProxyCommand=ssh -o StrictHostKeyChecking=no -W %h:%p ohri@20.172.182.46 -p 2202" \ - ohri@172.19.0.16 "\ - cd /opt && \ - docker compose -f docker-compose-ohri-dev.yml pull && \ - docker compose -f docker-compose-ohri-dev.yml stop && \ - docker compose -f docker-compose-ohri-dev.yml up -d" diff --git a/.gitignore b/.gitignore index 110fa84c7..5f4e45d03 100644 --- a/.gitignore +++ b/.gitignore @@ -77,3 +77,10 @@ build/** # vscode .vscode + +# Playwright and e2e tests +/test-results/ +/playwright-report/ +/playwright/.cache/ +e2e/storageState.json +.env \ No newline at end of file diff --git a/__mocks__/encounter-list.mock.tsx b/__mocks__/encounter-list.mock.tsx new file mode 100644 index 000000000..b7d9a2215 --- /dev/null +++ b/__mocks__/encounter-list.mock.tsx @@ -0,0 +1,1252 @@ +import { getEncounterValues } from '@ohri/openmrs-esm-ohri-commons-lib'; + +export const mockEncounterType = '15272be5-ae9c-4278-a303-4b8907eae73b'; + +export const mockPatientUuid = '1b2278d5-c9ea-4f00-bfb1-60af48dc838a'; + +export const mockColumns = [ + { + key: 'deathDate', + header: 'Death Date', + getValue: () => '2024-01-01', + link: { + handleNavigate: (encounter) => { + encounter.launchFormActions?.viewEncounter(); + }, + }, + }, + { + key: 'deathCause', + header: 'Cause of Death', + getValue: () => '2024-01-01', + link: { + handleNavigate: (encounter) => { + encounter.launchFormActions?.viewEncounter(); + }, + }, + }, + { + key: 'specificDeathCause', + header: 'Specific cause of Death', + getValue: () => '2024-01-01', + link: { + handleNavigate: (encounter) => { + encounter.launchFormActions?.viewEncounter(); + }, + }, + }, + { + key: 'actions', + header: 'Actions', + getValue: () => '2024-01-01', + link: { + handleNavigate: (encounter) => { + encounter.launchFormActions?.viewEncounter(); + }, + }, + }, +]; + +export const mockForms = [ + { + name: 'Death Form', + uuid: 'some-uuid', + }, +]; + +export const mockEncounter = [ + { + uuid: 'ee106966-4cd9-4465-8ac2-dfac9e3751d3', + encounterDatetime: '2024-01-05T07:01:53.000+0000', + encounterType: { + uuid: '70d2b2f6-860b-438c-994b-c28e863dca34', + display: 'TB Contact Listing', + name: 'TB Contact Listing', + description: 'Use to list contact of a TB patient for testing', + retired: false, + links: [ + { + rel: 'self', + uri: 'https://ohri-dev.globalhealthapp.net/openmrs/ws/rest/v1/encountertype/70d2b2f6-860b-438c-994b-c28e863dca34', + resourceAlias: 'encountertype', + }, + { + rel: 'full', + uri: 'https://ohri-dev.globalhealthapp.net/openmrs/ws/rest/v1/encountertype/70d2b2f6-860b-438c-994b-c28e863dca34?v=full', + resourceAlias: 'encountertype', + }, + ], + resourceVersion: '1.8', + }, + location: { + uuid: '44c3efb0-2583-4c80-a79e-1f756a03c0a1', + name: 'Outpatient Clinic', + }, + patient: { + uuid: '1b2278d5-c9ea-4f00-bfb1-60af48dc838a', + display: '100002U - Clark Bob Robert', + }, + encounterProviders: [ + { + uuid: 'bc4ef577-ee0a-4288-85a7-77c753b3421e', + provider: { + uuid: 'bc450226-4138-40b7-ad88-9c98df687738', + name: 'Super User', + }, + }, + ], + obs: [ + { + uuid: '711b26c8-a91a-48e3-919c-c4888919d76d', + obsDatetime: '2024-01-05T07:01:53.000+0000', + voided: false, + groupMembers: null, + concept: { + uuid: '160753AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', + name: { + uuid: 'f415bf25-2008-3ac2-8f0e-46e75648e30d', + name: 'Date of event', + }, + }, + value: '2024-01-05T00:00:00.000+0000', + }, + { + uuid: '37094756-1d74-420c-a63c-fe8418ea3b7d', + obsDatetime: '2024-02-13T13:04:39.000+0000', + voided: false, + groupMembers: [ + { + uuid: '71abc700-a69a-40c2-8a0b-90e76abe3da2', + display: 'Address (text): test', + concept: { + uuid: '162725AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', + display: 'Address (text)', + links: [ + { + rel: 'self', + uri: 'https://ohri-dev.globalhealthapp.net/openmrs/ws/rest/v1/concept/162725AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', + resourceAlias: 'concept', + }, + ], + }, + person: { + uuid: '1b2278d5-c9ea-4f00-bfb1-60af48dc838a', + display: '100002U - Clark Bob Robert', + links: [ + { + rel: 'self', + uri: 'https://ohri-dev.globalhealthapp.net/openmrs/ws/rest/v1/patient/1b2278d5-c9ea-4f00-bfb1-60af48dc838a', + resourceAlias: 'patient', + }, + ], + }, + obsDatetime: '2024-01-05T07:01:53.000+0000', + accessionNumber: null, + obsGroup: { + uuid: '37094756-1d74-420c-a63c-fe8418ea3b7d', + display: 'Contact details: test, 0789000000, Yes, Family member, Test', + links: [ + { + rel: 'self', + uri: 'https://ohri-dev.globalhealthapp.net/openmrs/ws/rest/v1/obs/37094756-1d74-420c-a63c-fe8418ea3b7d', + resourceAlias: 'obs', + }, + ], + }, + valueCodedName: null, + groupMembers: null, + comment: null, + location: { + uuid: '44c3efb0-2583-4c80-a79e-1f756a03c0a1', + display: 'Outpatient Clinic', + links: [ + { + rel: 'self', + uri: 'https://ohri-dev.globalhealthapp.net/openmrs/ws/rest/v1/location/44c3efb0-2583-4c80-a79e-1f756a03c0a1', + resourceAlias: 'location', + }, + ], + }, + order: null, + encounter: { + uuid: 'ee106966-4cd9-4465-8ac2-dfac9e3751d3', + display: 'TB Contact Listing 05/01/2024', + links: [ + { + rel: 'self', + uri: 'https://ohri-dev.globalhealthapp.net/openmrs/ws/rest/v1/encounter/ee106966-4cd9-4465-8ac2-dfac9e3751d3', + resourceAlias: 'encounter', + }, + ], + }, + voided: false, + value: 'test', + valueModifier: null, + formFieldPath: 'ohri-forms-contactAddress', + formFieldNamespace: 'ohri-forms', + status: 'FINAL', + interpretation: null, + links: [ + { + rel: 'self', + uri: 'https://ohri-dev.globalhealthapp.net/openmrs/ws/rest/v1/obs/71abc700-a69a-40c2-8a0b-90e76abe3da2', + resourceAlias: 'obs', + }, + { + rel: 'full', + uri: 'https://ohri-dev.globalhealthapp.net/openmrs/ws/rest/v1/obs/71abc700-a69a-40c2-8a0b-90e76abe3da2?v=full', + resourceAlias: 'obs', + }, + ], + resourceVersion: '2.1', + }, + { + uuid: '35293a4d-039e-4d49-b192-c0864beac1df', + display: 'Contact phone number: 0789000000', + concept: { + uuid: '159635AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', + display: 'Contact phone number', + links: [ + { + rel: 'self', + uri: 'https://ohri-dev.globalhealthapp.net/openmrs/ws/rest/v1/concept/159635AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', + resourceAlias: 'concept', + }, + ], + }, + person: { + uuid: '1b2278d5-c9ea-4f00-bfb1-60af48dc838a', + display: '100002U - Clark Bob Robert', + links: [ + { + rel: 'self', + uri: 'https://ohri-dev.globalhealthapp.net/openmrs/ws/rest/v1/patient/1b2278d5-c9ea-4f00-bfb1-60af48dc838a', + resourceAlias: 'patient', + }, + ], + }, + obsDatetime: '2024-01-05T07:01:53.000+0000', + accessionNumber: null, + obsGroup: { + uuid: '37094756-1d74-420c-a63c-fe8418ea3b7d', + display: 'Contact details: test, 0789000000, Yes, Family member, Test', + links: [ + { + rel: 'self', + uri: 'https://ohri-dev.globalhealthapp.net/openmrs/ws/rest/v1/obs/37094756-1d74-420c-a63c-fe8418ea3b7d', + resourceAlias: 'obs', + }, + ], + }, + valueCodedName: null, + groupMembers: null, + comment: null, + location: { + uuid: '44c3efb0-2583-4c80-a79e-1f756a03c0a1', + display: 'Outpatient Clinic', + links: [ + { + rel: 'self', + uri: 'https://ohri-dev.globalhealthapp.net/openmrs/ws/rest/v1/location/44c3efb0-2583-4c80-a79e-1f756a03c0a1', + resourceAlias: 'location', + }, + ], + }, + order: null, + encounter: { + uuid: 'ee106966-4cd9-4465-8ac2-dfac9e3751d3', + display: 'TB Contact Listing 05/01/2024', + links: [ + { + rel: 'self', + uri: 'https://ohri-dev.globalhealthapp.net/openmrs/ws/rest/v1/encounter/ee106966-4cd9-4465-8ac2-dfac9e3751d3', + resourceAlias: 'encounter', + }, + ], + }, + voided: false, + value: '0789000000', + valueModifier: null, + formFieldPath: 'ohri-forms-contactPhoneNumber', + formFieldNamespace: 'ohri-forms', + status: 'FINAL', + interpretation: null, + links: [ + { + rel: 'self', + uri: 'https://ohri-dev.globalhealthapp.net/openmrs/ws/rest/v1/obs/35293a4d-039e-4d49-b192-c0864beac1df', + resourceAlias: 'obs', + }, + { + rel: 'full', + uri: 'https://ohri-dev.globalhealthapp.net/openmrs/ws/rest/v1/obs/35293a4d-039e-4d49-b192-c0864beac1df?v=full', + resourceAlias: 'obs', + }, + ], + resourceVersion: '2.1', + }, + { + uuid: 'a5cef1b1-9f44-4fa5-9cc8-07ba2e8a3fc4', + display: 'Contacts invited: Yes', + concept: { + uuid: '164072AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', + display: 'Contacts invited', + links: [ + { + rel: 'self', + uri: 'https://ohri-dev.globalhealthapp.net/openmrs/ws/rest/v1/concept/164072AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', + resourceAlias: 'concept', + }, + ], + }, + person: { + uuid: '1b2278d5-c9ea-4f00-bfb1-60af48dc838a', + display: '100002U - Clark Bob Robert', + links: [ + { + rel: 'self', + uri: 'https://ohri-dev.globalhealthapp.net/openmrs/ws/rest/v1/patient/1b2278d5-c9ea-4f00-bfb1-60af48dc838a', + resourceAlias: 'patient', + }, + ], + }, + obsDatetime: '2024-01-05T07:01:53.000+0000', + accessionNumber: null, + obsGroup: { + uuid: '37094756-1d74-420c-a63c-fe8418ea3b7d', + display: 'Contact details: test, 0789000000, Yes, Family member, Test', + links: [ + { + rel: 'self', + uri: 'https://ohri-dev.globalhealthapp.net/openmrs/ws/rest/v1/obs/37094756-1d74-420c-a63c-fe8418ea3b7d', + resourceAlias: 'obs', + }, + ], + }, + valueCodedName: null, + groupMembers: null, + comment: null, + location: { + uuid: '44c3efb0-2583-4c80-a79e-1f756a03c0a1', + display: 'Outpatient Clinic', + links: [ + { + rel: 'self', + uri: 'https://ohri-dev.globalhealthapp.net/openmrs/ws/rest/v1/location/44c3efb0-2583-4c80-a79e-1f756a03c0a1', + resourceAlias: 'location', + }, + ], + }, + order: null, + encounter: { + uuid: 'ee106966-4cd9-4465-8ac2-dfac9e3751d3', + display: 'TB Contact Listing 05/01/2024', + links: [ + { + rel: 'self', + uri: 'https://ohri-dev.globalhealthapp.net/openmrs/ws/rest/v1/encounter/ee106966-4cd9-4465-8ac2-dfac9e3751d3', + resourceAlias: 'encounter', + }, + ], + }, + voided: false, + value: { + uuid: '1065AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', + display: 'Yes', + name: { + display: 'Yes', + uuid: 'd8c3337b-a1cb-3519-af40-7a016eedb72f', + name: 'Yes', + locale: 'en', + localePreferred: true, + conceptNameType: 'FULLY_SPECIFIED', + links: [ + { + rel: 'self', + uri: 'https://ohri-dev.globalhealthapp.net/openmrs/ws/rest/v1/concept/1065AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/name/d8c3337b-a1cb-3519-af40-7a016eedb72f', + resourceAlias: 'name', + }, + { + rel: 'full', + uri: 'https://ohri-dev.globalhealthapp.net/openmrs/ws/rest/v1/concept/1065AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/name/d8c3337b-a1cb-3519-af40-7a016eedb72f?v=full', + resourceAlias: 'name', + }, + ], + resourceVersion: '1.9', + }, + datatype: { + uuid: '8d4a4c94-c2cc-11de-8d13-0010c6dffd0f', + display: 'N/A', + links: [ + { + rel: 'self', + uri: 'https://ohri-dev.globalhealthapp.net/openmrs/ws/rest/v1/conceptdatatype/8d4a4c94-c2cc-11de-8d13-0010c6dffd0f', + resourceAlias: 'conceptdatatype', + }, + ], + }, + conceptClass: { + uuid: '8d492774-c2cc-11de-8d13-0010c6dffd0f', + display: 'Misc', + links: [ + { + rel: 'self', + uri: 'https://ohri-dev.globalhealthapp.net/openmrs/ws/rest/v1/conceptclass/8d492774-c2cc-11de-8d13-0010c6dffd0f', + resourceAlias: 'conceptclass', + }, + ], + }, + set: false, + version: null, + retired: false, + names: [ + { + uuid: 'd8c3337b-a1cb-3519-af40-7a016eedb72f', + display: 'Yes', + links: [ + { + rel: 'self', + uri: 'https://ohri-dev.globalhealthapp.net/openmrs/ws/rest/v1/concept/1065AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/name/d8c3337b-a1cb-3519-af40-7a016eedb72f', + resourceAlias: 'name', + }, + ], + }, + ], + descriptions: [ + { + uuid: '24f4ab95-c95b-42cc-8839-7014b00987ce', + display: 'Generic answer to a question.', + links: [ + { + rel: 'self', + uri: 'https://ohri-dev.globalhealthapp.net/openmrs/ws/rest/v1/concept/1065AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/description/24f4ab95-c95b-42cc-8839-7014b00987ce', + resourceAlias: 'description', + }, + ], + }, + ], + mappings: [ + { + uuid: '564715f3-ea35-4e17-b3a7-c6212db334c1', + display: 'PIH: 1065', + links: [ + { + rel: 'self', + uri: 'https://ohri-dev.globalhealthapp.net/openmrs/ws/rest/v1/concept/1065AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/mapping/564715f3-ea35-4e17-b3a7-c6212db334c1', + resourceAlias: 'mapping', + }, + ], + }, + { + uuid: 'd7e61750-ff8a-4a57-aa99-a78f0d803ce6', + display: 'CIEL: 1065', + links: [ + { + rel: 'self', + uri: 'https://ohri-dev.globalhealthapp.net/openmrs/ws/rest/v1/concept/1065AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/mapping/d7e61750-ff8a-4a57-aa99-a78f0d803ce6', + resourceAlias: 'mapping', + }, + ], + }, + { + uuid: 'a574243a-154e-40d9-825a-7c3e51c0ca6f', + display: 'SNOMED CT: 373066001', + links: [ + { + rel: 'self', + uri: 'https://ohri-dev.globalhealthapp.net/openmrs/ws/rest/v1/concept/1065AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/mapping/a574243a-154e-40d9-825a-7c3e51c0ca6f', + resourceAlias: 'mapping', + }, + ], + }, + { + uuid: '6757c68b-2aa9-4080-a4ae-40b946081177', + display: 'AMPATH: 1065', + links: [ + { + rel: 'self', + uri: 'https://ohri-dev.globalhealthapp.net/openmrs/ws/rest/v1/concept/1065AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/mapping/6757c68b-2aa9-4080-a4ae-40b946081177', + resourceAlias: 'mapping', + }, + ], + }, + ], + answers: [], + setMembers: [], + attributes: [], + links: [ + { + rel: 'self', + uri: 'https://ohri-dev.globalhealthapp.net/openmrs/ws/rest/v1/concept/1065AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', + resourceAlias: 'concept', + }, + { + rel: 'full', + uri: 'https://ohri-dev.globalhealthapp.net/openmrs/ws/rest/v1/concept/1065AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA?v=full', + resourceAlias: 'concept', + }, + ], + resourceVersion: '2.0', + }, + valueModifier: null, + formFieldPath: 'ohri-forms-contactsInvited', + formFieldNamespace: 'ohri-forms', + status: 'FINAL', + interpretation: null, + links: [ + { + rel: 'self', + uri: 'https://ohri-dev.globalhealthapp.net/openmrs/ws/rest/v1/obs/a5cef1b1-9f44-4fa5-9cc8-07ba2e8a3fc4', + resourceAlias: 'obs', + }, + { + rel: 'full', + uri: 'https://ohri-dev.globalhealthapp.net/openmrs/ws/rest/v1/obs/a5cef1b1-9f44-4fa5-9cc8-07ba2e8a3fc4?v=full', + resourceAlias: 'obs', + }, + ], + resourceVersion: '2.1', + }, + { + uuid: 'd93d637a-f5ae-40f0-981b-7705e96ec833', + display: 'Relationship to Tuberculosis contact: Family member', + concept: { + uuid: '160239AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', + display: 'Relationship to Tuberculosis contact', + links: [ + { + rel: 'self', + uri: 'https://ohri-dev.globalhealthapp.net/openmrs/ws/rest/v1/concept/160239AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', + resourceAlias: 'concept', + }, + ], + }, + person: { + uuid: '1b2278d5-c9ea-4f00-bfb1-60af48dc838a', + display: '100002U - Clark Bob Robert', + links: [ + { + rel: 'self', + uri: 'https://ohri-dev.globalhealthapp.net/openmrs/ws/rest/v1/patient/1b2278d5-c9ea-4f00-bfb1-60af48dc838a', + resourceAlias: 'patient', + }, + ], + }, + obsDatetime: '2024-01-05T07:01:53.000+0000', + accessionNumber: null, + obsGroup: { + uuid: '37094756-1d74-420c-a63c-fe8418ea3b7d', + display: 'Contact details: test, 0789000000, Yes, Family member, Test', + links: [ + { + rel: 'self', + uri: 'https://ohri-dev.globalhealthapp.net/openmrs/ws/rest/v1/obs/37094756-1d74-420c-a63c-fe8418ea3b7d', + resourceAlias: 'obs', + }, + ], + }, + valueCodedName: null, + groupMembers: null, + comment: null, + location: { + uuid: '44c3efb0-2583-4c80-a79e-1f756a03c0a1', + display: 'Outpatient Clinic', + links: [ + { + rel: 'self', + uri: 'https://ohri-dev.globalhealthapp.net/openmrs/ws/rest/v1/location/44c3efb0-2583-4c80-a79e-1f756a03c0a1', + resourceAlias: 'location', + }, + ], + }, + order: null, + encounter: { + uuid: 'ee106966-4cd9-4465-8ac2-dfac9e3751d3', + display: 'TB Contact Listing 05/01/2024', + links: [ + { + rel: 'self', + uri: 'https://ohri-dev.globalhealthapp.net/openmrs/ws/rest/v1/encounter/ee106966-4cd9-4465-8ac2-dfac9e3751d3', + resourceAlias: 'encounter', + }, + ], + }, + voided: false, + value: { + uuid: '1560AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', + display: 'Family member', + name: { + display: 'Family member', + uuid: '634a1aa6-73dc-3ab7-a067-293ff62ca1e7', + name: 'Family member', + locale: 'en', + localePreferred: true, + conceptNameType: 'FULLY_SPECIFIED', + links: [ + { + rel: 'self', + uri: 'https://ohri-dev.globalhealthapp.net/openmrs/ws/rest/v1/concept/1560AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/name/634a1aa6-73dc-3ab7-a067-293ff62ca1e7', + resourceAlias: 'name', + }, + { + rel: 'full', + uri: 'https://ohri-dev.globalhealthapp.net/openmrs/ws/rest/v1/concept/1560AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/name/634a1aa6-73dc-3ab7-a067-293ff62ca1e7?v=full', + resourceAlias: 'name', + }, + ], + resourceVersion: '1.9', + }, + datatype: { + uuid: '8d4a48b6-c2cc-11de-8d13-0010c6dffd0f', + display: 'Coded', + links: [ + { + rel: 'self', + uri: 'https://ohri-dev.globalhealthapp.net/openmrs/ws/rest/v1/conceptdatatype/8d4a48b6-c2cc-11de-8d13-0010c6dffd0f', + resourceAlias: 'conceptdatatype', + }, + ], + }, + conceptClass: { + uuid: '8d491e50-c2cc-11de-8d13-0010c6dffd0f', + display: 'Question', + links: [ + { + rel: 'self', + uri: 'https://ohri-dev.globalhealthapp.net/openmrs/ws/rest/v1/conceptclass/8d491e50-c2cc-11de-8d13-0010c6dffd0f', + resourceAlias: 'conceptclass', + }, + ], + }, + set: false, + version: null, + retired: false, + names: [ + { + uuid: '634a1aa6-73dc-3ab7-a067-293ff62ca1e7', + display: 'Family member', + links: [ + { + rel: 'self', + uri: 'https://ohri-dev.globalhealthapp.net/openmrs/ws/rest/v1/concept/1560AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/name/634a1aa6-73dc-3ab7-a067-293ff62ca1e7', + resourceAlias: 'name', + }, + ], + }, + ], + descriptions: [], + mappings: [ + { + uuid: '20f1cf1f-c948-4fd7-8123-bebe110beb9a', + display: 'CIEL: 1560', + links: [ + { + rel: 'self', + uri: 'https://ohri-dev.globalhealthapp.net/openmrs/ws/rest/v1/concept/1560AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/mapping/20f1cf1f-c948-4fd7-8123-bebe110beb9a', + resourceAlias: 'mapping', + }, + ], + }, + { + uuid: '58c0fe00-b0e8-4bea-8ed0-86bab365642f', + display: 'PIH: 6441', + links: [ + { + rel: 'self', + uri: 'https://ohri-dev.globalhealthapp.net/openmrs/ws/rest/v1/concept/1560AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/mapping/58c0fe00-b0e8-4bea-8ed0-86bab365642f', + resourceAlias: 'mapping', + }, + ], + }, + { + uuid: 'fbd918f2-3e31-45ee-8de2-30663bb545b4', + display: 'SNOMED CT: 303071001', + links: [ + { + rel: 'self', + uri: 'https://ohri-dev.globalhealthapp.net/openmrs/ws/rest/v1/concept/1560AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/mapping/fbd918f2-3e31-45ee-8de2-30663bb545b4', + resourceAlias: 'mapping', + }, + ], + }, + ], + answers: [ + { + uuid: '975AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', + display: 'Aunt', + links: [ + { + rel: 'self', + uri: 'https://ohri-dev.globalhealthapp.net/openmrs/ws/rest/v1/concept/975AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', + resourceAlias: 'concept', + }, + ], + }, + { + uuid: '160729AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', + display: 'Brother', + links: [ + { + rel: 'self', + uri: 'https://ohri-dev.globalhealthapp.net/openmrs/ws/rest/v1/concept/160729AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', + resourceAlias: 'concept', + }, + ], + }, + { + uuid: '1528AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', + display: 'Child', + links: [ + { + rel: 'self', + uri: 'https://ohri-dev.globalhealthapp.net/openmrs/ws/rest/v1/concept/1528AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', + resourceAlias: 'concept', + }, + ], + }, + { + uuid: '160728AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', + display: 'Daughter', + links: [ + { + rel: 'self', + uri: 'https://ohri-dev.globalhealthapp.net/openmrs/ws/rest/v1/concept/160728AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', + resourceAlias: 'concept', + }, + ], + }, + { + uuid: '971AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', + display: 'Father', + links: [ + { + rel: 'self', + uri: 'https://ohri-dev.globalhealthapp.net/openmrs/ws/rest/v1/concept/971AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', + resourceAlias: 'concept', + }, + ], + }, + { + uuid: '160273AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', + display: 'Grandfather', + links: [ + { + rel: 'self', + uri: 'https://ohri-dev.globalhealthapp.net/openmrs/ws/rest/v1/concept/160273AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', + resourceAlias: 'concept', + }, + ], + }, + { + uuid: '159772AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', + display: 'Grandmother', + links: [ + { + rel: 'self', + uri: 'https://ohri-dev.globalhealthapp.net/openmrs/ws/rest/v1/concept/159772AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', + resourceAlias: 'concept', + }, + ], + }, + { + uuid: '973AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', + display: 'Grandparent', + links: [ + { + rel: 'self', + uri: 'https://ohri-dev.globalhealthapp.net/openmrs/ws/rest/v1/concept/973AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', + resourceAlias: 'concept', + }, + ], + }, + { + uuid: '160724AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', + display: 'Maternal grandfather', + links: [ + { + rel: 'self', + uri: 'https://ohri-dev.globalhealthapp.net/openmrs/ws/rest/v1/concept/160724AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', + resourceAlias: 'concept', + }, + ], + }, + { + uuid: '160723AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', + display: 'Maternal grandmother', + links: [ + { + rel: 'self', + uri: 'https://ohri-dev.globalhealthapp.net/openmrs/ws/rest/v1/concept/160723AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', + resourceAlias: 'concept', + }, + ], + }, + { + uuid: '970AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', + display: 'Mother', + links: [ + { + rel: 'self', + uri: 'https://ohri-dev.globalhealthapp.net/openmrs/ws/rest/v1/concept/970AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', + resourceAlias: 'concept', + }, + ], + }, + { + uuid: '5620AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', + display: 'Other family member', + links: [ + { + rel: 'self', + uri: 'https://ohri-dev.globalhealthapp.net/openmrs/ws/rest/v1/concept/5620AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', + resourceAlias: 'concept', + }, + ], + }, + { + uuid: '1527AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', + display: 'Parent', + links: [ + { + rel: 'self', + uri: 'https://ohri-dev.globalhealthapp.net/openmrs/ws/rest/v1/concept/1527AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', + resourceAlias: 'concept', + }, + ], + }, + { + uuid: '5617AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', + display: 'Partner or spouse', + links: [ + { + rel: 'self', + uri: 'https://ohri-dev.globalhealthapp.net/openmrs/ws/rest/v1/concept/5617AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', + resourceAlias: 'concept', + }, + ], + }, + { + uuid: '160725AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', + display: 'Paternal grandfather', + links: [ + { + rel: 'self', + uri: 'https://ohri-dev.globalhealthapp.net/openmrs/ws/rest/v1/concept/160725AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', + resourceAlias: 'concept', + }, + ], + }, + { + uuid: '160726AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', + display: 'Paternal grandmother', + links: [ + { + rel: 'self', + uri: 'https://ohri-dev.globalhealthapp.net/openmrs/ws/rest/v1/concept/160726AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', + resourceAlias: 'concept', + }, + ], + }, + { + uuid: '972AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', + display: 'Sibling', + links: [ + { + rel: 'self', + uri: 'https://ohri-dev.globalhealthapp.net/openmrs/ws/rest/v1/concept/972AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', + resourceAlias: 'concept', + }, + ], + }, + { + uuid: '160730AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', + display: 'Sister', + links: [ + { + rel: 'self', + uri: 'https://ohri-dev.globalhealthapp.net/openmrs/ws/rest/v1/concept/160730AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', + resourceAlias: 'concept', + }, + ], + }, + { + uuid: '160727AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', + display: 'Son', + links: [ + { + rel: 'self', + uri: 'https://ohri-dev.globalhealthapp.net/openmrs/ws/rest/v1/concept/160727AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', + resourceAlias: 'concept', + }, + ], + }, + { + uuid: '974AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', + display: 'Uncle', + links: [ + { + rel: 'self', + uri: 'https://ohri-dev.globalhealthapp.net/openmrs/ws/rest/v1/concept/974AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', + resourceAlias: 'concept', + }, + ], + }, + { + uuid: '970AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', + display: 'Mother', + links: [ + { + rel: 'self', + uri: 'https://ohri-dev.globalhealthapp.net/openmrs/ws/rest/v1/concept/970AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', + resourceAlias: 'concept', + }, + ], + }, + { + uuid: '971AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', + display: 'Father', + links: [ + { + rel: 'self', + uri: 'https://ohri-dev.globalhealthapp.net/openmrs/ws/rest/v1/concept/971AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', + resourceAlias: 'concept', + }, + ], + }, + { + uuid: '159772AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', + display: 'Grandmother', + links: [ + { + rel: 'self', + uri: 'https://ohri-dev.globalhealthapp.net/openmrs/ws/rest/v1/concept/159772AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', + resourceAlias: 'concept', + }, + ], + }, + { + uuid: '160273AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', + display: 'Grandfather', + links: [ + { + rel: 'self', + uri: 'https://ohri-dev.globalhealthapp.net/openmrs/ws/rest/v1/concept/160273AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', + resourceAlias: 'concept', + }, + ], + }, + { + uuid: '972AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', + display: 'Sibling', + links: [ + { + rel: 'self', + uri: 'https://ohri-dev.globalhealthapp.net/openmrs/ws/rest/v1/concept/972AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', + resourceAlias: 'concept', + }, + ], + }, + { + uuid: '1528AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', + display: 'Child', + links: [ + { + rel: 'self', + uri: 'https://ohri-dev.globalhealthapp.net/openmrs/ws/rest/v1/concept/1528AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', + resourceAlias: 'concept', + }, + ], + }, + { + uuid: '975AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', + display: 'Aunt', + links: [ + { + rel: 'self', + uri: 'https://ohri-dev.globalhealthapp.net/openmrs/ws/rest/v1/concept/975AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', + resourceAlias: 'concept', + }, + ], + }, + { + uuid: '974AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', + display: 'Uncle', + links: [ + { + rel: 'self', + uri: 'https://ohri-dev.globalhealthapp.net/openmrs/ws/rest/v1/concept/974AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', + resourceAlias: 'concept', + }, + ], + }, + { + uuid: '5620AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', + display: 'Other family member', + links: [ + { + rel: 'self', + uri: 'https://ohri-dev.globalhealthapp.net/openmrs/ws/rest/v1/concept/5620AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', + resourceAlias: 'concept', + }, + ], + }, + { + uuid: '160726AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', + display: 'Paternal grandmother', + links: [ + { + rel: 'self', + uri: 'https://ohri-dev.globalhealthapp.net/openmrs/ws/rest/v1/concept/160726AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', + resourceAlias: 'concept', + }, + ], + }, + { + uuid: '160723AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', + display: 'Maternal grandmother', + links: [ + { + rel: 'self', + uri: 'https://ohri-dev.globalhealthapp.net/openmrs/ws/rest/v1/concept/160723AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', + resourceAlias: 'concept', + }, + ], + }, + { + uuid: '160725AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', + display: 'Paternal grandfather', + links: [ + { + rel: 'self', + uri: 'https://ohri-dev.globalhealthapp.net/openmrs/ws/rest/v1/concept/160725AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', + resourceAlias: 'concept', + }, + ], + }, + { + uuid: '160724AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', + display: 'Maternal grandfather', + links: [ + { + rel: 'self', + uri: 'https://ohri-dev.globalhealthapp.net/openmrs/ws/rest/v1/concept/160724AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', + resourceAlias: 'concept', + }, + ], + }, + { + uuid: '160727AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', + display: 'Son', + links: [ + { + rel: 'self', + uri: 'https://ohri-dev.globalhealthapp.net/openmrs/ws/rest/v1/concept/160727AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', + resourceAlias: 'concept', + }, + ], + }, + { + uuid: '160728AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', + display: 'Daughter', + links: [ + { + rel: 'self', + uri: 'https://ohri-dev.globalhealthapp.net/openmrs/ws/rest/v1/concept/160728AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', + resourceAlias: 'concept', + }, + ], + }, + { + uuid: '160729AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', + display: 'Brother', + links: [ + { + rel: 'self', + uri: 'https://ohri-dev.globalhealthapp.net/openmrs/ws/rest/v1/concept/160729AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', + resourceAlias: 'concept', + }, + ], + }, + { + uuid: '160730AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', + display: 'Sister', + links: [ + { + rel: 'self', + uri: 'https://ohri-dev.globalhealthapp.net/openmrs/ws/rest/v1/concept/160730AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', + resourceAlias: 'concept', + }, + ], + }, + { + uuid: '5617AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', + display: 'Partner or spouse', + links: [ + { + rel: 'self', + uri: 'https://ohri-dev.globalhealthapp.net/openmrs/ws/rest/v1/concept/5617AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', + resourceAlias: 'concept', + }, + ], + }, + { + uuid: '973AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', + display: 'Grandparent', + links: [ + { + rel: 'self', + uri: 'https://ohri-dev.globalhealthapp.net/openmrs/ws/rest/v1/concept/973AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', + resourceAlias: 'concept', + }, + ], + }, + { + uuid: '1527AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', + display: 'Parent', + links: [ + { + rel: 'self', + uri: 'https://ohri-dev.globalhealthapp.net/openmrs/ws/rest/v1/concept/1527AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', + resourceAlias: 'concept', + }, + ], + }, + ], + setMembers: [], + attributes: [], + links: [ + { + rel: 'self', + uri: 'https://ohri-dev.globalhealthapp.net/openmrs/ws/rest/v1/concept/1560AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', + resourceAlias: 'concept', + }, + { + rel: 'full', + uri: 'https://ohri-dev.globalhealthapp.net/openmrs/ws/rest/v1/concept/1560AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA?v=full', + resourceAlias: 'concept', + }, + ], + resourceVersion: '2.0', + }, + valueModifier: null, + formFieldPath: 'ohri-forms-tbRelationship', + formFieldNamespace: 'ohri-forms', + status: 'FINAL', + interpretation: null, + links: [ + { + rel: 'self', + uri: 'https://ohri-dev.globalhealthapp.net/openmrs/ws/rest/v1/obs/d93d637a-f5ae-40f0-981b-7705e96ec833', + resourceAlias: 'obs', + }, + { + rel: 'full', + uri: 'https://ohri-dev.globalhealthapp.net/openmrs/ws/rest/v1/obs/d93d637a-f5ae-40f0-981b-7705e96ec833?v=full', + resourceAlias: 'obs', + }, + ], + resourceVersion: '2.1', + }, + { + uuid: 'a8f75b70-2102-4d91-b946-d075477e153e', + display: 'Name of contact person: Test', + concept: { + uuid: '163258AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', + display: 'Name of contact person', + links: [ + { + rel: 'self', + uri: 'https://ohri-dev.globalhealthapp.net/openmrs/ws/rest/v1/concept/163258AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', + resourceAlias: 'concept', + }, + ], + }, + person: { + uuid: '1b2278d5-c9ea-4f00-bfb1-60af48dc838a', + display: '100002U - Clark Bob Robert', + links: [ + { + rel: 'self', + uri: 'https://ohri-dev.globalhealthapp.net/openmrs/ws/rest/v1/patient/1b2278d5-c9ea-4f00-bfb1-60af48dc838a', + resourceAlias: 'patient', + }, + ], + }, + obsDatetime: '2024-01-05T07:01:53.000+0000', + accessionNumber: null, + obsGroup: { + uuid: '37094756-1d74-420c-a63c-fe8418ea3b7d', + display: 'Contact details: test, 0789000000, Yes, Family member, Test', + links: [ + { + rel: 'self', + uri: 'https://ohri-dev.globalhealthapp.net/openmrs/ws/rest/v1/obs/37094756-1d74-420c-a63c-fe8418ea3b7d', + resourceAlias: 'obs', + }, + ], + }, + valueCodedName: null, + groupMembers: null, + comment: null, + location: { + uuid: '44c3efb0-2583-4c80-a79e-1f756a03c0a1', + display: 'Outpatient Clinic', + links: [ + { + rel: 'self', + uri: 'https://ohri-dev.globalhealthapp.net/openmrs/ws/rest/v1/location/44c3efb0-2583-4c80-a79e-1f756a03c0a1', + resourceAlias: 'location', + }, + ], + }, + order: null, + encounter: { + uuid: 'ee106966-4cd9-4465-8ac2-dfac9e3751d3', + display: 'TB Contact Listing 05/01/2024', + links: [ + { + rel: 'self', + uri: 'https://ohri-dev.globalhealthapp.net/openmrs/ws/rest/v1/encounter/ee106966-4cd9-4465-8ac2-dfac9e3751d3', + resourceAlias: 'encounter', + }, + ], + }, + voided: false, + value: 'Test', + valueModifier: null, + formFieldPath: 'ohri-forms-nameofContactPerson', + formFieldNamespace: 'ohri-forms', + status: 'FINAL', + interpretation: null, + links: [ + { + rel: 'self', + uri: 'https://ohri-dev.globalhealthapp.net/openmrs/ws/rest/v1/obs/a8f75b70-2102-4d91-b946-d075477e153e', + resourceAlias: 'obs', + }, + { + rel: 'full', + uri: 'https://ohri-dev.globalhealthapp.net/openmrs/ws/rest/v1/obs/a8f75b70-2102-4d91-b946-d075477e153e?v=full', + resourceAlias: 'obs', + }, + ], + resourceVersion: '2.1', + }, + ], + concept: { + uuid: '164351AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', + name: { + uuid: 'd878b58c-51e1-3987-a3d7-96d7af4fba58', + name: 'Contact details', + }, + }, + value: null, + }, + ], + form: { + uuid: 'cb16d920-62f1-3696-b781-e6a4f5e80de1', + name: 'TB Contact Listing', + }, + launchFormActions: {}, + }, +]; diff --git a/e2e/README.md b/e2e/README.md new file mode 100644 index 000000000..32a0d90c3 --- /dev/null +++ b/e2e/README.md @@ -0,0 +1,117 @@ +# E2E Tests + +This directory contains an E2E test suite using the [Playwright](https://playwright.dev) +framework. + +## Getting Started + +Please ensure that you have followed the basic installation guide in the [root README](../README.md). Once everything is set up, make sure the dev server is running by using: + +```sh +yarn start +``` + +Then, in a separate terminal, run: + +```sh +yarn test-e2e --headed +``` + +By default, the test suite will run against the http://localhost:8080. You can override this by exporting `E2E_BASE_URL` environment variables beforehand: + +```sh +# Ex: Set the server URL to dev3: +export E2E_BASE_URL=https://dev3.openmrs.org/openmrs + +# Run all e2e tests: + +```sh +yarn test-e2e --headed +``` + +To run a specific test by title: + +```sh +yarn test-e2e --headed -g "title of the test" +``` + +Check [this documentation](https://playwright.dev/docs/running-tests#command-line) for more running options. + +It is also highly recommended to install the companion VS Code extension: +https://playwright.dev/docs/getting-started-vscode + +## Writing New Tests + +In general, it is recommended to read through the official [Playwright docs](https://playwright.dev/docs/intro) +before writing new test cases. The project uses the official Playwright test runner and, +generally, follows a very simple project structure: + +``` +e2e +|__ commands +| ^ Contains "commands" (simple reusable functions) that can be used in test cases/specs, +| e.g. generate a random patient. +|__ core +| ^ Contains code related to the test runner itself, e.g. setting up the custom fixtures. +| You probably need to touch this infrequently. +|__ fixtures +| ^ Contains fixtures (https://playwright.dev/docs/test-fixtures) which are used +| to run reusable setup/teardown tasks +|__ pages +| ^ Contains page object model classes for interacting with the frontend. +| See https://playwright.dev/docs/test-pom for details. +|__ specs +| ^ Contains the actual test cases/specs. New tests should be placed in this folder. +|__ support + ^ Contains support files that requires to run e2e tests, e.g. docker compose files. +``` + +When you want to write a new test case, start by creating a new spec in `./specs`. +Depending on what you want to achieve, you might want to create new fixtures and/or +page object models. To see examples, have a look at the existing code to see how these +different concepts play together. + +## Open reports from GitHub Actions / Bamboo + +To download the report from the GitHub action/Bamboo plan, follow these steps: + +1. Go to the artifact section of the action/plan and locate the report file. +2. Download the report file and unzip it using a tool of your choice. +3. Open the index.html file in a web browser to view the report. + +The report will show you a full summary of your tests, including information on which +tests passed, failed, were skipped, or were flaky. You can filter the report by browser +and explore the details of individual tests, including any errors or failures, video +recordings, and the steps involved in each test. Simply click on a test to view its details. + +## Debugging Tests + +Refer to [this documentation](https://playwright.dev/docs/debug) on how to debug a test. + +## Configuration + +This is very much underdeveloped/WIP. At the moment, there exists a (git-shared) `.env` +file which can be used for configuring certain test attributes. This is most likely +about to change in the future. Stay tuned for updates! + +## Github Actions integration + +The e2e.yml workflow is made up of two jobs: one for running on pull requests (PRs) and +one for running on commits. + +1. When running on PRs, the workflow will start the dev server, use dev3.openmrs.org as the backend, +and run tests only on chromium. This is done in order to quickly provide feedback to the developer. +The tests are designed to generate their own data and clean up after themselves once they are finished. +This ensures that the tests will have minimum effect from changes made to dev3 by other developers. +In the future, we plan to use a docker container to run the tests in an isolated environment once we +figure out a way to spin up the container within a small amount of time. +2. When running on commits, the workflow will spin up a docker container and run the dev server against +it in order to provide a known and isolated environment. In addition, tests will be run on multiple +browsers (chromium, firefox, and WebKit) to ensure compatibility. + +## Troubleshooting tips + +On MacOS, you might run into the following error: +```browserType.launch: Executable doesn't exist at /Users//Library/Caches/ms-playwright/chromium-1015/chrome-mac/Chromium.app/Contents/MacOS/Chromium``` +In order to fix this, you can attempt to force the browser reinstallation by running: +```PLAYWRIGHT_BROWSERS_PATH=/Users/$USER/Library/Caches/ms-playwright npx playwright install``` diff --git a/e2e/commands/index.ts b/e2e/commands/index.ts new file mode 100644 index 000000000..a3640e32c --- /dev/null +++ b/e2e/commands/index.ts @@ -0,0 +1,2 @@ +export * from './patient-operations'; +export * from './visit-operations'; diff --git a/e2e/commands/patient-operations.ts b/e2e/commands/patient-operations.ts new file mode 100644 index 000000000..f04171b57 --- /dev/null +++ b/e2e/commands/patient-operations.ts @@ -0,0 +1,105 @@ +import { APIRequestContext, expect } from '@playwright/test'; + +export interface Patient { + uuid: string; + identifiers: Identifier[]; + display: string; + person: { + uuid: string; + display: string; + gender: string; + age: number; + birthdate: string; + birthdateEstimated: boolean; + dead: boolean; + deathDate?: string; + causeOfDeath?: string; + preferredAddress: { + address1: string; + cityVillage: string; + country: string; + postalCode: string; + stateProvince: string; + countyDistrict: string; + }; + attributes: Array>; + voided: boolean; + birthtime?: string; + deathdateEstimated: boolean; + resourceVersion: string; + }; + attributes: { value: string; attributeType: { uuid: string; display: string } }[]; + voided: boolean; +} + +export interface Address { + preferred: boolean; + address1: string; + cityVillage: string; + country: string; + postalCode: string; + stateProvince: string; +} + +export interface Identifier { + uuid: string; + display: string; +} + +export const generateRandomPatient = async (api: APIRequestContext): Promise => { + const identifierRes = await api.post('idgen/identifiersource/8549f706-7e85-4c1d-9424-217d50a2988b/identifier', { + data: {}, + }); + await expect(identifierRes.ok()).toBeTruthy(); + const { identifier } = await identifierRes.json(); + + const patientRes = await api.post('patient', { + // TODO: This is not configurable right now. It probably should be. + data: { + identifiers: [ + { + identifier, + identifierType: '05a29f94-c0ed-11e2-94be-8c13b969e334', + location: '44c3efb0-2583-4c80-a79e-1f756a03c0a1', + preferred: true, + }, + ], + person: { + addresses: [ + { + address1: 'Bom Jesus Street', + address2: '', + cityVillage: 'Recife', + country: 'Brazil', + postalCode: '50030-310', + stateProvince: 'Pernambuco', + }, + ], + attributes: [], + birthdate: '2020-2-1', + birthdateEstimated: true, + dead: false, + gender: 'M', + names: [ + { + familyName: `Smith${Math.floor(Math.random() * 10000)}`, + givenName: `John${Math.floor(Math.random() * 10000)}`, + middleName: '', + preferred: true, + }, + ], + }, + }, + }); + await expect(patientRes.ok()).toBeTruthy(); + return await patientRes.json(); +}; + +export const getPatient = async (api: APIRequestContext, uuid: string): Promise => { + const patientRes = await api.get(`patient/${uuid}?v=full`); + return await patientRes.json(); +}; + +export const deletePatient = async (api: APIRequestContext, uuid: string) => { + await api.delete(`patient/${uuid}`, { data: {} }); +}; diff --git a/e2e/commands/visit-operations.ts b/e2e/commands/visit-operations.ts new file mode 100644 index 000000000..5aff42094 --- /dev/null +++ b/e2e/commands/visit-operations.ts @@ -0,0 +1,31 @@ +import { APIRequestContext, expect } from '@playwright/test'; +import { Visit } from '@openmrs/esm-framework'; +import dayjs from 'dayjs'; + +export const startVisit = async (api: APIRequestContext, patientId: string): Promise => { + const visitRes = await api.post('visit', { + data: { + startDatetime: dayjs().subtract(1, 'D').format('YYYY-MM-DDTHH:mm:ss.SSSZZ'), + patient: patientId, + location: process.env.E2E_LOGIN_DEFAULT_LOCATION_UUID, + visitType: '7b0f5697-27e3-40c4-8bae-f4049abfb4ed', + attributes: [], + }, + }); + + await expect(visitRes.ok()).toBeTruthy(); + return await visitRes.json(); +}; + +export const endVisit = async (api: APIRequestContext, uuid: string) => { + const visitRes = await api.post(`visit/${uuid}`, { + data: { + location: process.env.E2E_LOGIN_DEFAULT_LOCATION_UUID, + startDatetime: dayjs().subtract(1, 'D').format('YYYY-MM-DDTHH:mm:ss.SSSZZ'), + visitType: '7b0f5697-27e3-40c4-8bae-f4049abfb4ed', + stopDatetime: dayjs().format('YYYY-MM-DDTHH:mm:ss.SSSZZ'), + }, + }); + + return await visitRes.json(); +}; diff --git a/e2e/core/global-setup.ts b/e2e/core/global-setup.ts new file mode 100644 index 000000000..8d8bae0af --- /dev/null +++ b/e2e/core/global-setup.ts @@ -0,0 +1,32 @@ +import { request } from '@playwright/test'; +import * as dotenv from 'dotenv'; + +dotenv.config(); + +/** + * This configuration is to reuse the signed-in state in the tests + * by log in only once using the API and then skip the log in step for all the tests. + * + * https://playwright.dev/docs/auth#reuse-signed-in-state + */ + +async function globalSetup() { + const requestContext = await request.newContext(); + const token = Buffer.from(`${process.env.E2E_USER_ADMIN_USERNAME}:${process.env.E2E_USER_ADMIN_PASSWORD}`).toString( + 'base64', + ); + await requestContext.post(`${process.env.E2E_BASE_URL}/ws/rest/v1/session`, { + data: { + sessionLocation: process.env.E2E_LOGIN_DEFAULT_LOCATION_UUID, + locale: 'en', + }, + headers: { + Accept: 'application/json', + Authorization: `Basic ${token}`, + }, + }); + await requestContext.storageState({ path: 'e2e/storageState.json' }); + await requestContext.dispose(); +} + +export default globalSetup; \ No newline at end of file diff --git a/e2e/core/index.ts b/e2e/core/index.ts new file mode 100644 index 000000000..607718c2a --- /dev/null +++ b/e2e/core/index.ts @@ -0,0 +1 @@ +export * from './test'; diff --git a/e2e/core/test.ts b/e2e/core/test.ts new file mode 100644 index 000000000..dd3e40bb4 --- /dev/null +++ b/e2e/core/test.ts @@ -0,0 +1,20 @@ +import { APIRequestContext, Page, test as base } from '@playwright/test'; +import { api } from '../fixtures'; + +// This file sets up our custom test harness using the custom fixtures. +// See https://playwright.dev/docs/test-fixtures#creating-a-fixture for details. +// If a spec intends to use one of the custom fixtures, the special `test` function +// exported from this file must be used instead of the default `test` function +// provided by playwright. + +export interface CustomTestFixtures { + loginAsAdmin: Page; +} + +export interface CustomWorkerFixtures { + api: APIRequestContext; +} + +export const test = base.extend({ + api: [api, { scope: 'worker' }], +}); diff --git a/e2e/fixtures/api.ts b/e2e/fixtures/api.ts new file mode 100644 index 000000000..6afd8d59e --- /dev/null +++ b/e2e/fixtures/api.ts @@ -0,0 +1,26 @@ +import { APIRequestContext, PlaywrightWorkerArgs, WorkerFixture } from '@playwright/test'; + +/** + * A fixture which initializes an [`APIRequestContext`](https://playwright.dev/docs/api/class-apirequestcontext) + * that is bound to the configured OpenMRS API server. The context is automatically authenticated + * using the configured admin account. + * + * Use the request context like this: + * ```ts + * test('your test', async ({ api }) => { + * const res = await api.get('patient/1234'); + * await expect(res.ok()).toBeTruthy(); + * }); + * ``` + */ +export const api: WorkerFixture = async ({ playwright }, use) => { + const ctx = await playwright.request.newContext({ + baseURL: `${process.env.E2E_BASE_URL}/ws/rest/v1/`, + httpCredentials: { + username: process.env.E2E_USER_ADMIN_USERNAME, + password: process.env.E2E_USER_ADMIN_PASSWORD, + }, + }); + + await use(ctx); +}; diff --git a/e2e/fixtures/index.ts b/e2e/fixtures/index.ts new file mode 100644 index 000000000..b1c13e734 --- /dev/null +++ b/e2e/fixtures/index.ts @@ -0,0 +1 @@ +export * from './api'; diff --git a/e2e/pages/chart-page.ts b/e2e/pages/chart-page.ts new file mode 100644 index 000000000..cd5191a41 --- /dev/null +++ b/e2e/pages/chart-page.ts @@ -0,0 +1,11 @@ +import { type Page } from '@playwright/test'; + +export class ChartPage { + constructor(readonly page: Page) {} + + readonly formsTable = () => this.page.getByRole('table', { name: /forms/i }); + + async goTo(patientUuid: string) { + await this.page.goto('/openmrs/spa/patient/' + patientUuid + '/chart'); + } +} diff --git a/e2e/pages/home-page.ts b/e2e/pages/home-page.ts new file mode 100644 index 000000000..c38ae88f2 --- /dev/null +++ b/e2e/pages/home-page.ts @@ -0,0 +1,11 @@ +import { Page } from '@playwright/test'; + +export class HomePage { + constructor(readonly page: Page) {} + + async gotoHome() { + await this.page.goto('/openmrs/spa/home'); + + } +} + diff --git a/e2e/pages/index.ts b/e2e/pages/index.ts new file mode 100644 index 000000000..d591c5ed3 --- /dev/null +++ b/e2e/pages/index.ts @@ -0,0 +1,3 @@ +export * from './home-page'; +export * from './chart-page'; +export * from './visits-page'; diff --git a/e2e/pages/visits-page.ts b/e2e/pages/visits-page.ts new file mode 100644 index 000000000..91b36bf65 --- /dev/null +++ b/e2e/pages/visits-page.ts @@ -0,0 +1,9 @@ +import { type Page } from '@playwright/test'; + +export class VisitsPage { + constructor(readonly page: Page) {} + + async goTo(patientUuid: string) { + await this.page.goto(`patient/${patientUuid}/chart/Visits`); + } +} diff --git a/e2e/specs/registration-test.spec.ts b/e2e/specs/registration-test.spec.ts new file mode 100644 index 000000000..4834fb5da --- /dev/null +++ b/e2e/specs/registration-test.spec.ts @@ -0,0 +1,40 @@ +import { test } from '../core'; +import { expect } from '@playwright/test'; +import { HomePage, ChartPage } from '../pages'; +import { type Patient, generateRandomPatient, startVisit } from '../commands'; +import { Visit } from '@openmrs/esm-framework'; + +let patient: Patient; +let visit: Visit; + +test.beforeEach(async ({ api }) => { + patient = await generateRandomPatient(api); + visit = await startVisit(api, patient.uuid); +}); + +test('Go to homepage and register patient', async ({ page }) => { + const homePage = new HomePage(page); + + await test.step('When I visit the home page', async () => { + await homePage.gotoHome(); + }); + + await test.step('Then should be at the home page', async () => { + await expect(page).toHaveURL(`${process.env.E2E_BASE_URL}/spa/home`); + }); + + await page.getByLabel('Add Patient').click(); + await page.getByLabel('First Name').click(); + await page.getByLabel('First Name').fill('daisy'); + await page.getByLabel('Family Name').click(); + await page.getByLabel('Family Name').fill('daisy'); + await page.getByText('Female').click(); + await page.getByRole('tab', { name: 'No' }).nth(1).click(); + await page.getByLabel('Estimated age in years').click(); + await page.getByLabel('Estimated age in years').fill('20'); + + // Click on register patient button + + await page.getByRole('button', { name: 'Register Patient' }).click(); + +}); diff --git a/frontend/spa-build-config.json b/frontend/spa-build-config.json index 52b5d9405..35a595280 100644 --- a/frontend/spa-build-config.json +++ b/frontend/spa-build-config.json @@ -1,48 +1,46 @@ { "frontendModules": { - "@ohri/openmrs-esm-ohri-core-app": "next", - "@ohri/openmrs-esm-ohri-form-render-app": "next", - "@ohri/openmrs-esm-ohri-hiv-app": "next", - "@ohri/openmrs-esm-ohri-covid-app": "next", - "@ohri/openmrs-esm-ohri-pmtct": "next", - "@ohri/openmrs-esm-ohri-tb-app": "next", - "@ohri/openmrs-esm-ohri-cervical-cancer-app": "next", - "@openmrs/esm-appointments-app": "next", - "@openmrs/esm-dispensing-app": "next", - "@openmrs/esm-form-builder-app": "next", - "@openmrs/esm-home-app": "next", - "@openmrs/esm-implementer-tools-app": "next", - "@openmrs/esm-login-app": "next", - "@openmrs/esm-outpatient-app": "next", - "@openmrs/esm-patient-allergies-app": "next", - "@openmrs/esm-patient-appointments-app": "next", - "@openmrs/esm-patient-attachments-app": "next", - "@openmrs/esm-patient-banner-app": "next", - "@openmrs/esm-patient-chart-app": "next", - "@openmrs/esm-patient-conditions-app": "next", - "@openmrs/esm-patient-forms-app": "next", - "@openmrs/esm-patient-immunizations-app": "next", - "@openmrs/esm-patient-list-app": "next", - "@openmrs/esm-patient-medications-app": "next", - "@openmrs/esm-patient-notes-app": "next", - "@openmrs/esm-patient-programs-app": "next", - "@openmrs/esm-patient-search-app": "next", - "@openmrs/esm-patient-test-results-app": "next", - "@openmrs/esm-patient-vitals-app": "next", - "@openmrs/esm-primary-navigation-app": "next", - "@openmrs/esm-patient-registration-app": "next", - "@openmrs/esm-devtools-app": "next", - "@openmrs/esm-openconceptlab-app": "next", - "@openmrs/esm-generic-patient-widgets-app": "next", - "@openmrs/esm-patient-lists-app": "next", - "@openmrs/esm-cohort-builder-app": "next", - "@openmrs/esm-patient-list-management-app": "next", - "@openmrs/esm-service-queues-app": "next", - "@openmrs/esm-form-entry-app": "next", - "@openmrs/esm-patient-labs-app": "next", - "@openmrs/esm-fast-data-entry-app": "next", - "@openmrs/esm-patient-orders-app": "next", - "@openmrs/esm-system-admin-app": "next" + "@openmrs/esm-devtools-app": "5.3.3-pre.1301", + "@openmrs/esm-implementer-tools-app": "5.3.3-pre.1301", + "@openmrs/esm-login-app": "5.3.3-pre.1301", + "@openmrs/esm-primary-navigation-app": "5.3.3-pre.1301", + "@openmrs/esm-home-app": "5.2.1-pre.305", + "@openmrs/esm-form-entry-app": "6.1.1-pre.3293", + "@openmrs/esm-generic-patient-widgets-app": "6.1.1-pre.3293", + "@openmrs/esm-patient-allergies-app": "6.1.1-pre.3293", + "@openmrs/esm-patient-appointments-app": "6.1.1-pre.3293", + "@openmrs/esm-patient-attachments-app": "6.1.1-pre.3293", + "@openmrs/esm-patient-banner-app": "6.1.1-pre.3293", + "@openmrs/esm-patient-chart-app": "6.1.1-pre.3293", + "@openmrs/esm-patient-conditions-app": "6.1.1-pre.3293", + "@openmrs/esm-patient-forms-app": "6.1.1-pre.3293", + "@openmrs/esm-patient-flags-app": "6.1.1-pre.3293", + "@openmrs/esm-patient-labs-app": "6.1.1-pre.3293", + "@openmrs/esm-patient-lists-app": "6.1.1-pre.3293", + "@openmrs/esm-patient-medications-app": "6.1.1-pre.3293", + "@openmrs/esm-patient-notes-app": "6.1.1-pre.3293", + "@openmrs/esm-patient-orders-app": "6.1.1-pre.3293", + "@openmrs/esm-patient-programs-app": "6.1.1-pre.3293", + "@openmrs/esm-patient-vitals-app": "6.1.1-pre.3293", + "@openmrs/esm-active-visits-app": "5.2.2-pre.2432", + "@openmrs/esm-appointments-app": "5.2.2-pre.2432", + "@openmrs/esm-service-queues-app": "5.2.2-pre.2432", + "@openmrs/esm-patient-list-management-app": "5.2.2-pre.2432", + "@openmrs/esm-patient-registration-app": "5.2.2-pre.2432", + "@openmrs/esm-patient-search-app": "5.2.2-pre.2432", + "@openmrs/esm-openconceptlab-app": "4.0.2-pre.88", + "@openmrs/esm-system-admin-app": "4.0.2-pre.88", + "@openmrs/esm-dispensing-app": "1.2.2-pre.256", + "@openmrs/esm-fast-data-entry-app": "1.0.1-pre.117", + "@openmrs/esm-cohort-builder-app": "3.0.1-pre.183", + "@openmrs/esm-form-builder-app": "2.2.2-pre.654", + "@ohri/openmrs-esm-ohri-core-app": "2.1.0-pre.3192", + "@ohri/openmrs-esm-ohri-hiv-app": "2.1.0-pre.3192", + "@ohri/openmrs-esm-ohri-covid-app": "2.1.0-pre.3192", + "@ohri/openmrs-esm-ohri-pmtct": "2.1.0-pre.3192", + "@ohri/openmrs-esm-ohri-tb-app": "2.1.0-pre.3192", + "@ohri/openmrs-esm-ohri-cervical-cancer-app": "2.1.0-pre.3192", + "@ohri/openmrs-esm-ohri-form-render-app": "2.1.0-pre.3192" }, "spaPath": "$SPA_PATH", "apiUrl": "$API_URL", diff --git a/jest.config.js b/jest.config.js index d87388e8c..82b65f646 100644 --- a/jest.config.js +++ b/jest.config.js @@ -35,6 +35,10 @@ const config = { '^react-i18next$': path.resolve(__dirname, '__mocks__', 'react-i18next.js'), }, testEnvironment: 'jsdom', + testPathIgnorePatterns: [ + "/node_modules/", + "/e2e/" // Ignore the e2e directory containing Playwright tests + ] }; module.exports = config; diff --git a/package.json b/package.json index 9f3a39da4..c47927628 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@ohri/openmrs-esm-ohri", "private": true, - "version": "2.2.0", + "version": "2.3.3", "description": "OHRI MicroFrontend for OpenMRS SPA", "workspaces": [ "packages/*" @@ -13,14 +13,15 @@ "access": "public" }, "scripts": { - "start": "openmrs develop --backend https://ohri-dev.globalhealthapp.net/ --sources 'packages/esm-*-app/'", - "start:core": "openmrs develop --backend https://ohri-dev.globalhealthapp.net --sources packages/esm-ohri-core-app", - "start:covid": "openmrs develop --backend https://ohri-dev.globalhealthapp.net --sources packages/esm-covid-app", - "start:hiv": "openmrs develop --backend https://ohri-dev.globalhealthapp.net --sources packages/esm-hiv-app", - "start:cervical-cancer": "openmrs develop --backend https://ohri-dev.globalhealthapp.net --sources packages/esm-cervical-cancer-app", + "start": "openmrs develop --backend https://openmrs-dev.globalhealthapp.net/ --sources 'packages/esm-*-app/'", + "start:core": "openmrs develop --backend https://openmrs-dev.globalhealthapp.net --sources packages/esm-ohri-core-app", + "start:covid": "openmrs develop --backend https://openmrs-dev.globalhealthapp.net --sources packages/esm-covid-app", + "start:hiv": "openmrs develop --backend https://openmrs-dev.globalhealthapp.net --sources packages/esm-hiv-app", + "start:cervical-cancer": "openmrs develop --backend https://openmrs-dev.globalhealthapp.net --sources packages/esm-cervical-cancer-app", "start:pmtct": "openmrs develop --backend https://ohri-namibia-dev.globalhealthapp.net --sources 'packages/esm-ohri-pmtct-app/'", - "start:form-render": "openmrs develop --backend https://ohri-dev.globalhealthapp.net --sources packages/esm-form-render-app", - "start:tb": "openmrs develop --backend https://ohri-dev.globalhealthapp.net --sources packages/esm-tb-app", + "start:form-render": "openmrs develop --backend https://openmrs-dev.globalhealthapp.net --sources packages/esm-form-render-app", + "start:tb": "openmrs develop --backend https://openmrs-dev.globalhealthapp.net --sources packages/esm-tb-app", + "start:opd": "openmrs develop --backend https://openmrs-dev.globalhealthapp.net --sources packages/esm-opd-app", "prettier": "prettier --fix --config prettier.config.js --write \"packages/**/*.{ts,tsx}\"", "prepare": "husky install", "test": "jest --config jest.config.js --verbose false --passWithNoTests", @@ -32,11 +33,13 @@ "coverage": "yarn test --coverage", "badges": "yarn coverage && yarn jest-coverage-badges --output './badges' ", "extract-translations": "turbo extract-translations -- --config ../../tools/i18next-parser.config.js", - "ci:bump-form-engine-lib": "yarn up @openmrs/openmrs-form-engine-lib@next" + "ci:bump-form-engine-lib": "yarn up @openmrs/openmrs-form-engine-lib@next", + "test-e2e": "playwright test" }, "devDependencies": { "@openmrs/esm-framework": "next", "@openmrs/esm-patient-common-lib": "next", + "@playwright/test": "^1.30.0", "@swc/cli": "^0.1.57", "@swc/core": "^1.2.189", "@swc/jest": "^0.2.29", @@ -60,6 +63,7 @@ "cross-env": "^7.0.3", "css-loader": "^6.8.1", "dayjs": "^1.11.10", + "dotenv": "^16.0.3", "eslint": "^8.55.0", "eslint-config-prettier": "^9.1.0", "eslint-config-ts-react-important-stuff": "^3.0.0", diff --git a/packages/esm-cervical-cancer-app/package.json b/packages/esm-cervical-cancer-app/package.json index b1bf3efcc..40dcb1901 100644 --- a/packages/esm-cervical-cancer-app/package.json +++ b/packages/esm-cervical-cancer-app/package.json @@ -1,6 +1,6 @@ { "name": "@ohri/openmrs-esm-ohri-cervical-cancer-app", - "version": "2.2.0", + "version": "2.3.3", "description": "Cervical cancer microfrontend for OpenMRS HIV Reference Implementation (OHRI)", "browser": "dist/openmrs-esm-ohri-cervical-cancer-app.js", "main": "src/index.ts", diff --git a/packages/esm-cervical-cancer-app/src/config-schema.ts b/packages/esm-cervical-cancer-app/src/config-schema.ts index c4a3612b7..8160ac0a7 100644 --- a/packages/esm-cervical-cancer-app/src/config-schema.ts +++ b/packages/esm-cervical-cancer-app/src/config-schema.ts @@ -33,6 +33,14 @@ export const configSchema = { cervicalCancerRegistrationForm: 'Cervical Cancer Registration Form', }, }, + formUuids: { + _type: Type.Object, + _description: 'List of uuids of forms related to Cacx.', + _default: { + screeningAndCancerTreatmentFormUuid: '9e3ec2a7-ad26-3f43-9677-82e318996eec', + cervicalCancerRegistrationFormUuid: '12f41bfe-6430-3d8c-9edf-2d1b7c904f0f', + }, + }, }; export interface ConfigObject { @@ -40,4 +48,5 @@ export interface ConfigObject { encounterTypes: Object; obsConcepts: Object; formNames: Object; + formUuids: Object; } diff --git a/packages/esm-cervical-cancer-app/src/views/cacx-visits/tabs/cacx-registration.component.tsx b/packages/esm-cervical-cancer-app/src/views/cacx-visits/tabs/cacx-registration.component.tsx index a471915c0..db64f392c 100644 --- a/packages/esm-cervical-cancer-app/src/views/cacx-visits/tabs/cacx-registration.component.tsx +++ b/packages/esm-cervical-cancer-app/src/views/cacx-visits/tabs/cacx-registration.component.tsx @@ -20,6 +20,8 @@ export const CacxRegistration: React.FC = ({ patientUuid const { cervicalCancerRegistrationForm } = config.formNames; + const { cervicalCancerRegistrationFormUuid } = config.formUuids; + const columnsLab: EncounterListColumn[] = useMemo( () => [ { @@ -77,7 +79,7 @@ export const CacxRegistration: React.FC = ({ patientUuid = ({ patientUuid }) => const { screeningAndCancerTreatmentForm } = config.formNames; + const { screeningAndCancerTreatmentFormUuid } = config.formUuids; + const columnsLab: EncounterListColumn[] = useMemo( () => [ { @@ -100,7 +102,7 @@ export const CacxTreatment: React.FC = ({ patientUuid }) => = ({ tableHeaders, tableRows }) => { - return ( diff --git a/packages/esm-commons-lib/src/components/encounter-list/encounter-list.component.tsx b/packages/esm-commons-lib/src/components/encounter-list/encounter-list.component.tsx index ab3050434..a849aa717 100644 --- a/packages/esm-commons-lib/src/components/encounter-list/encounter-list.component.tsx +++ b/packages/esm-commons-lib/src/components/encounter-list/encounter-list.component.tsx @@ -1,18 +1,19 @@ -import { navigate } from '@openmrs/esm-framework'; +import { navigate, showModal, showSnackbar } from '@openmrs/esm-framework'; import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { EmptyState } from '../empty-state/empty-state.component'; -import { OHRIFormLauncherWithIntent } from '../ohri-form-launcher/ohri-form-launcher.component'; +import { FormLauncherWithIntent } from '../ohri-form-launcher/ohri-form-launcher.component'; import styles from './encounter-list.scss'; import { OTable } from '../data-table/o-table.component'; import { Button, Link, OverflowMenu, OverflowMenuItem, Pagination, DataTableSkeleton } from '@carbon/react'; import { Add } from '@carbon/react/icons'; -import { OHRIFormSchema } from '@openmrs/openmrs-form-engine-lib'; -import { launchEncounterForm } from './helpers'; +import { FormSchema } from '@openmrs/openmrs-form-engine-lib'; +import { deleteEncounter, launchEncounterForm } from './helpers'; import { useEncounterRows } from '../../hooks/useEncounterRows'; import { OpenmrsEncounter } from '../../api/types'; import { useFormsJson } from '../../hooks/useFormsJson'; import { usePatientDeathStatus } from '../../hooks/usePatientDeathStatus'; +import { mutate } from 'swr'; export interface EncounterListColumn { key: string; @@ -29,6 +30,7 @@ export interface EncounterListProps { description: string; formList?: Array<{ name: string; + uuid?: string; excludedIntents?: Array; fixedIntent?: string; isDefault?: boolean; @@ -54,18 +56,14 @@ export const EncounterList: React.FC = ({ }) => { const { t } = useTranslation(); const [paginatedRows, setPaginatedRows] = useState([]); - const [forms, setForms] = useState([]); + const [forms, setForms] = useState([]); const [currentPage, setCurrentPage] = useState(1); const [pageSize, setPageSize] = useState(10); const [isLoadingForms, setIsLoadingForms] = useState(true); const { isDead } = usePatientDeathStatus(patientUuid); const formNames = useMemo(() => formList.map((form) => form.name), []); const { formsJson, isLoading: isLoadingFormsJson } = useFormsJson(formNames); - const { - encounters, - isLoading: isLoadingEncounters, - onFormSave, - } = useEncounterRows(patientUuid, encounterType, filter); + const { encounters, isLoading, onFormSave } = useEncounterRows(patientUuid, encounterType, filter); const { moduleName, workspaceWindowSize, displayText, hideFormLauncher } = launchOptions; const defaultActions = useMemo( @@ -83,13 +81,55 @@ export const EncounterList: React.FC = ({ form: { name: forms[0]?.name, }, - mode: 'view', + mode: 'edit', + intent: '*', + }, + { + label: t('deleteEncounter', 'Delete'), + form: { + name: forms[0]?.name, + }, + mode: 'delete', intent: '*', }, ], [forms, t], ); + const handleDeleteEncounter = useCallback((encounterUuid, encounterTypeName) => { + const close = showModal('delete-encounter-modal', { + close: () => close(), + encounterTypeName: encounterTypeName || '', + onConfirmation: () => { + const abortController = new AbortController(); + deleteEncounter(encounterUuid, abortController) + .then(() => { + mutate( + (key) => + typeof key === "string" && key.startsWith("/ws/rest/v1/encounter"), + undefined, + { revalidate: true } + ); + showSnackbar({ + isLowContrast: true, + title: t('encounterDeleted', 'Encounter deleted'), + subtitle: `Encounter ${t('successfullyDeleted', 'successfully deleted')}`, + kind: 'success', + }); + }) + .catch(() => { + showSnackbar({ + isLowContrast: false, + title: t('error', 'Error'), + subtitle: `Encounter ${t('failedDeleting', "couldn't be deleted")}`, + kind: 'error', + }); + }); + close(); + }, + }); + }, []) + useEffect(() => { if (!isLoadingFormsJson) { const formsWithFilteredIntents = formsJson.map((form) => { @@ -145,6 +185,7 @@ export const EncounterList: React.FC = ({ encounter.uuid, null, workspaceWindowSize, + patientUuid, ), viewEncounter: () => launchEncounterForm( @@ -156,11 +197,12 @@ export const EncounterList: React.FC = ({ encounter.uuid, null, workspaceWindowSize, + patientUuid, ), }; // process columns columns.forEach((column) => { - let val = column.getValue(encounter); + let val = column?.getValue(encounter); if (column.link) { val = ( = ({ // If custom config is available, generate actions accordingly; otherwise, fallback to the default actions. const actions = tableRow.actions?.length ? tableRow.actions : defaultActions; tableRow['actions'] = ( - + {actions.map((actionItem, index) => ( { e.preventDefault(); - launchEncounterForm( - forms.find((form) => form.name == actionItem?.form?.name), - moduleName, - actionItem.mode == 'enter' ? 'add' : actionItem.mode, - onFormSave, - null, - encounter.uuid, - actionItem.intent, - workspaceWindowSize, - ); + actionItem.mode == 'delete' ? + handleDeleteEncounter(encounter.uuid, encounter.encounterType.name) + : launchEncounterForm( + forms.find((form) => form.name == actionItem?.form?.name), + moduleName, + actionItem.mode == 'enter' ? 'add' : actionItem.mode, + onFormSave, + null, + encounter.uuid, + actionItem.intent, + workspaceWindowSize, + patientUuid, + ); }} /> ))} @@ -206,7 +252,7 @@ export const EncounterList: React.FC = ({ }); setPaginatedRows(rows); }, - [columns, defaultActions, forms, moduleName, workspaceWindowSize], + [columns, defaultActions, forms, moduleName, workspaceWindowSize, patientUuid, onFormSave], ); useEffect(() => { @@ -219,40 +265,64 @@ export const EncounterList: React.FC = ({ if (forms.length == 1 && !forms[0]['availableIntents']?.length) { // we only have one form with no intents // just return the "Add" button - return ( + return ( ); } else if (forms.length && !(hideFormLauncher ?? isDead)) { return ( - - launchEncounterForm(formJson, moduleName, 'add', onFormSave, null, null, intent, workspaceWindowSize) + launchEncounterForm( + formJson, + moduleName, + 'add', + onFormSave, + null, + '', + intent, + workspaceWindowSize, + patientUuid, + ) } title={displayText} /> ); } - }, [forms, hideFormLauncher, isDead, displayText, moduleName, workspaceWindowSize]); + return null; + }, [forms, hideFormLauncher, isDead, displayText, moduleName, workspaceWindowSize, onFormSave, patientUuid]); + + if (isLoading === true || isLoadingForms === true || isLoadingFormsJson === true) { + return ; + } return ( <> - {isLoadingEncounters || isLoadingForms ? ( - - ) : encounters.length > 0 ? ( + {paginatedRows?.length > 0 || encounters.length > 0 ? ( <>

{headerTitle}

+ {/* @ts-ignore */} {!(hideFormLauncher ?? isDead) &&
{formLauncher}
}
@@ -273,7 +343,17 @@ export const EncounterList: React.FC = ({ displayText={description} headerTitle={headerTitle} launchForm={() => - launchEncounterForm(forms[0], moduleName, 'add', onFormSave, null, null, '*', workspaceWindowSize) + launchEncounterForm( + forms[0], + moduleName, + 'add', + onFormSave, + null, + '', + '*', + workspaceWindowSize, + patientUuid, + ) } launchFormComponent={formLauncher} hideFormLauncher={hideFormLauncher ?? isDead} diff --git a/packages/esm-commons-lib/src/components/encounter-list/encounter-list.test.tsx b/packages/esm-commons-lib/src/components/encounter-list/encounter-list.test.tsx new file mode 100644 index 000000000..6c776c1c7 --- /dev/null +++ b/packages/esm-commons-lib/src/components/encounter-list/encounter-list.test.tsx @@ -0,0 +1,107 @@ +import '@testing-library/jest-dom'; +import React from 'react'; +import { act, render, screen } from '@testing-library/react'; +import { EncounterList } from './encounter-list.component'; +import { openmrsFetch, usePagination } from '@openmrs/esm-framework'; +import { mockColumns, mockEncounter, mockEncounterType, mockForms } from '../../../../../__mocks__/encounter-list.mock'; +import { launchPatientWorkspace } from '@openmrs/esm-patient-common-lib'; +import * as encounterRowsHook from '../../hooks/useEncounterRows'; +import * as formsJsonHook from '../../hooks/useFormsJson'; + +const emptyTestProps = { + formConceptMap: {}, + patientUuid: 'some-uuid', + encounterType: mockEncounterType, + columns: [], + headerTitle: 'Sample header title encounter list', + description: 'Sample description encounter list', + formList: [], + filter: jest.fn(), + launchOptions: { + moduleName: '', + hideFormLauncher: false, + displayText: '', + }, +}; + +const testProps = { + formConceptMap: {}, + patientUuid: 'some-uuid', + encounterType: mockEncounterType, + columns: mockColumns, + headerTitle: 'Sample header title encounter list', + description: 'Sample description encounter list', + formList: mockForms, + filter: jest.fn(), + launchOptions: { + moduleName: '', + hideFormLauncher: false, + displayText: '', + }, +}; + +jest.mock('../../hooks/useEncounterRows'); +jest.mock('../../hooks/useFormsJson'); + +jest.mock('@openmrs/esm-patient-common-lib', () => ({ + launchPatientWorkspace: jest.fn(), +})); + +jest.mock('@openmrs/openmrs-form-engine-lib', () => ({ + FormEngine: jest + .fn() + .mockImplementation(() => React.createElement('div', { 'data-testid': 'openmrs form' }, 'FORM ENGINE LIB')), +})); + +describe('EncounterList', () => { + afterEach(() => { + jest.clearAllMocks(); + }); + + test('renders an loading state if data is loading', () => { + jest + .spyOn(encounterRowsHook, 'useEncounterRows') + .mockReturnValue({ encounters: [], isLoading: true, error: null, onFormSave: () => {} }); + + jest.spyOn(formsJsonHook, 'useFormsJson').mockReturnValue({ formsJson: [], isLoading: true }); + + act(() => { + render(); + }); + const element = document.querySelector('.cds--skeleton.cds--data-table-container'); + expect(element).not.toBeNull(); + }); + + test('renders an empty state if data is null', () => { + jest + .spyOn(encounterRowsHook, 'useEncounterRows') + .mockReturnValue({ encounters: [], isLoading: false, error: null, onFormSave: () => {} }); + + jest.spyOn(formsJsonHook, 'useFormsJson').mockReturnValue({ formsJson: [], isLoading: false }); + + act(() => { + render(); + }); + expect( + screen.getByText('There are no sample description encounter list to display for this patient'), + ).toBeInTheDocument(); + }); + + test('should render the encounter list component', () => { + jest.spyOn(encounterRowsHook, 'useEncounterRows').mockReturnValue({ + encounters: mockEncounter, + isLoading: false, + error: null, + onFormSave: () => {}, + }); + jest.spyOn(formsJsonHook, 'useFormsJson').mockReturnValue({ formsJson: [], isLoading: false }); + + act(() => { + render(); + }); + expect(screen.getByText('Sample header title encounter list')).toBeInTheDocument(); + expect(screen.getByText('Death Date')).toBeInTheDocument(); + expect(screen.getByText('Click to sort rows by Cause of Death header in ascending order')).toBeInTheDocument(); + expect(screen.getByText('Cause of Death')).toBeInTheDocument(); + }); +}); diff --git a/packages/esm-commons-lib/src/components/encounter-list/helpers.ts b/packages/esm-commons-lib/src/components/encounter-list/helpers.ts index 04730dd31..747329e3b 100644 --- a/packages/esm-commons-lib/src/components/encounter-list/helpers.ts +++ b/packages/esm-commons-lib/src/components/encounter-list/helpers.ts @@ -1,11 +1,11 @@ -import { OHRIFormSchema, SessionMode } from '@openmrs/openmrs-form-engine-lib'; -import { launchForm } from '../../utils/ohri-forms-commons'; -import { capitalize } from 'lodash-es'; +import { openmrsFetch, restBaseUrl } from '@openmrs/esm-framework'; +import { launchPatientWorkspace } from '@openmrs/esm-patient-common-lib'; +import { FormSchema } from '@openmrs/openmrs-form-engine-lib'; -type LaunchAction = 'add' | 'view' | 'edit'; +type LaunchAction = 'add' | 'view' | 'edit' | 'embedded-view'; export function launchEncounterForm( - form: OHRIFormSchema, + form: FormSchema, moduleName: string, action: LaunchAction = 'add', onFormSave: () => void, @@ -13,16 +13,33 @@ export function launchEncounterForm( encounterUuid?: string, intent: string = '*', workspaceWindowSize?: 'minimized' | 'maximized', + patientUuid?: string, ) { - const defaultTitle = capitalize(action) + ' ' + form.name; - launchForm( - form, - action === 'add' ? 'enter' : action, - moduleName, - title || defaultTitle, - encounterUuid, - intent, - onFormSave, - workspaceWindowSize, - ); + launchPatientWorkspace('patient-form-entry-workspace', { + workspaceTitle: form.name, + mutateForm: onFormSave, + formInfo: { + encounterUuid, + formUuid: form.name, + patientUuid: patientUuid, + visitTypeUuid: '', + visitUuid: '', + visitStartDatetime: '', + visitStopDatetime: '', + additionalProps: { + mode: action === 'add' ? 'enter' : action, + formSessionIntent: intent, + }, + }, + }); } + +export function deleteEncounter(encounterUuid: string, abortController: AbortController) { + return openmrsFetch(`${restBaseUrl}/encounter/${encounterUuid}`, { + method: 'DELETE', + headers: { + 'Content-Type': 'application/json', + }, + signal: abortController.signal, + }); +} diff --git a/packages/esm-commons-lib/src/components/encounter-tile/encounter-tile.component.tsx b/packages/esm-commons-lib/src/components/encounter-tile/encounter-tile.component.tsx index d8b3e2b27..ac1f84686 100644 --- a/packages/esm-commons-lib/src/components/encounter-tile/encounter-tile.component.tsx +++ b/packages/esm-commons-lib/src/components/encounter-tile/encounter-tile.component.tsx @@ -56,7 +56,7 @@ export const EncounterValuesTile: React.FC = ({ patien --
- ) + ); } return ( diff --git a/packages/esm-commons-lib/src/components/ohri-form-launcher/ohri-form-empty-launcher.component.tsx b/packages/esm-commons-lib/src/components/ohri-form-launcher/ohri-form-empty-launcher.component.tsx index 185f5def3..1ac693f2c 100644 --- a/packages/esm-commons-lib/src/components/ohri-form-launcher/ohri-form-empty-launcher.component.tsx +++ b/packages/esm-commons-lib/src/components/ohri-form-launcher/ohri-form-empty-launcher.component.tsx @@ -3,7 +3,7 @@ import { Button } from '@carbon/react'; import { Add } from '@carbon/react/icons'; import { useTranslation } from 'react-i18next'; -export const OHRIFormLauncherEmpty: React.FC<{ +export const FormLauncherEmpty: React.FC<{ launchForm: (formJson?: any) => void; }> = ({ launchForm }) => { const { t } = useTranslation(); diff --git a/packages/esm-commons-lib/src/components/ohri-form-launcher/ohri-form-launcher.component.tsx b/packages/esm-commons-lib/src/components/ohri-form-launcher/ohri-form-launcher.component.tsx index 29208a2f0..0bb844250 100644 --- a/packages/esm-commons-lib/src/components/ohri-form-launcher/ohri-form-launcher.component.tsx +++ b/packages/esm-commons-lib/src/components/ohri-form-launcher/ohri-form-launcher.component.tsx @@ -4,7 +4,7 @@ import styles from './launcher-with-intent.scss'; import { useTranslation } from 'react-i18next'; import { OHRIOverflowMenu } from '../overflow-menu-button/ohri-overflow-menu.component'; -export const OHRIFormLauncherWithIntent: React.FC<{ +export const FormLauncherWithIntent: React.FC<{ launchForm: (formJson?: any, intent?: string) => void; title?: string; formJsonList?: Array; diff --git a/packages/esm-commons-lib/src/components/ohri-home/welcome-section/ohri-welcome-section.scss b/packages/esm-commons-lib/src/components/ohri-home/welcome-section/ohri-welcome-section.scss index d93a73e6c..ccefc064b 100644 --- a/packages/esm-commons-lib/src/components/ohri-home/welcome-section/ohri-welcome-section.scss +++ b/packages/esm-commons-lib/src/components/ohri-home/welcome-section/ohri-welcome-section.scss @@ -1,6 +1,7 @@ .welcomeContainer { display: flex; flex-direction: column; + margin: 1rem; } .location { diff --git a/packages/esm-commons-lib/src/components/tile/ohri-programme-summary-tiles.scss b/packages/esm-commons-lib/src/components/tile/ohri-programme-summary-tiles.scss index e9845e821..042124f96 100644 --- a/packages/esm-commons-lib/src/components/tile/ohri-programme-summary-tiles.scss +++ b/packages/esm-commons-lib/src/components/tile/ohri-programme-summary-tiles.scss @@ -1,6 +1,7 @@ .desktopView { display: flex; gap: 8px; + margin: 1rem; } .tileView { diff --git a/packages/esm-commons-lib/src/hooks/useEncounterRows.ts b/packages/esm-commons-lib/src/hooks/useEncounterRows.ts index dc99bb2c9..d2a6dd05b 100644 --- a/packages/esm-commons-lib/src/hooks/useEncounterRows.ts +++ b/packages/esm-commons-lib/src/hooks/useEncounterRows.ts @@ -1,4 +1,4 @@ -import useSWRImmutable, { mutate } from 'swr'; +import useSWR from 'swr'; import { OpenmrsEncounter } from '../api/types'; import { useCallback, useEffect, useMemo, useState } from 'react'; import { openmrsFetch } from '@openmrs/esm-framework'; @@ -6,16 +6,14 @@ import { encounterRepresentation } from '../constants'; export function useEncounterRows(patientUuid: string, encounterType: string, encounterFilter: (encounter) => boolean) { const [encounters, setEncounters] = useState([]); - const url = useMemo( - () => `/ws/rest/v1/encounter?encounterType=${encounterType}&patient=${patientUuid}&v=${encounterRepresentation}`, - [encounterType, patientUuid], - ); + const url = `/ws/rest/v1/encounter?encounterType=${encounterType}&patient=${patientUuid}&v=${encounterRepresentation}`; const { data: response, error, isLoading, - } = useSWRImmutable<{ data: { results: OpenmrsEncounter[] } }, Error>(url, openmrsFetch); + mutate, + } = useSWR<{ data: { results: OpenmrsEncounter[] } }, Error>(url, openmrsFetch); useEffect(() => { if (response) { @@ -33,8 +31,8 @@ export function useEncounterRows(patientUuid: string, encounterType: string, enc }, [encounterFilter, response]); const onFormSave = useCallback(() => { - mutate(url); - }, [url]); + mutate(); + }, [mutate]); return { encounters, diff --git a/packages/esm-commons-lib/src/hooks/useFormsJson.ts b/packages/esm-commons-lib/src/hooks/useFormsJson.ts index 09984ea28..c4057b463 100644 --- a/packages/esm-commons-lib/src/hooks/useFormsJson.ts +++ b/packages/esm-commons-lib/src/hooks/useFormsJson.ts @@ -17,7 +17,7 @@ export function useFormsJson(formNames: string[]) { setOpenmrsForms( responses .map((response, index) => { - const match = response.data.results.find((result) => !result.retired && result.name === formNames[index]); + const match = response?.data?.results.find((result) => !result.retired && result.name === formNames[index]); if (!match) { console.error('Form not found: ' + formNames[index]); return null; diff --git a/packages/esm-commons-lib/src/hooks/useLastEncounter.tsx b/packages/esm-commons-lib/src/hooks/useLastEncounter.tsx index 4ffce9a65..ea177c183 100644 --- a/packages/esm-commons-lib/src/hooks/useLastEncounter.tsx +++ b/packages/esm-commons-lib/src/hooks/useLastEncounter.tsx @@ -5,7 +5,7 @@ import useSWR from 'swr'; export function useLastEncounter(patientUuid: string, encounterType: string) { const query = `encounterType=${encounterType}&patient=${patientUuid}&limit=1&order=desc&startIndex=0`; - const endpointUrl = `/ws/rest/v1/encounter?${query}&v=${encounterRepresentation}`; + const endpointUrl = `/ws/rest/v1/encounter?${query}&v=${encounterRepresentation}`; const { data, error, isValidating } = useSWR<{ data: { results: Array } }, Error>( endpointUrl, diff --git a/packages/esm-commons-lib/src/hooks/usePatientDeathStatus.ts b/packages/esm-commons-lib/src/hooks/usePatientDeathStatus.ts index 36a6e3726..f51385c48 100644 --- a/packages/esm-commons-lib/src/hooks/usePatientDeathStatus.ts +++ b/packages/esm-commons-lib/src/hooks/usePatientDeathStatus.ts @@ -1,20 +1,14 @@ import { openmrsFetch } from '@openmrs/esm-framework'; -import { useEffect, useState } from 'react'; import useSWRImmutable from 'swr'; export function usePatientDeathStatus(patientUuid: string) { - const [isDead, setIsDead] = useState(false); - const { data: response } = useSWRImmutable( - `/ws/rest/v1/person/${patientUuid}?v=custom:(dead)`, - openmrsFetch, - ); + const { + data: response, + isLoading, + error, + } = useSWRImmutable(`/ws/rest/v1/person/${patientUuid}?v=custom:(dead)`, openmrsFetch); - useEffect(() => { - if (response) { - setIsDead(response.data.dead); - } - }, [response]); return { - isDead, + isDead: !isLoading && !error && response ? response?.data?.dead : false, }; } diff --git a/packages/esm-commons-lib/src/index.ts b/packages/esm-commons-lib/src/index.ts index becd0da5d..7c87c1f6f 100644 --- a/packages/esm-commons-lib/src/index.ts +++ b/packages/esm-commons-lib/src/index.ts @@ -29,6 +29,8 @@ export * from './components/tile/ohri-summary-tile-tablet.component'; export * from './components/tile/ohri-summary-tile.component'; export * from './utils/compare'; export * from './utils/createOHRIDashboardLink'; +export * from './utils/createNewOHRIDashboardLink'; +export * from './utils/createOHRIGroupedLink'; export * from './utils/events'; export * from './utils/get-dosage'; export * from './utils/helpers'; diff --git a/packages/esm-commons-lib/src/root.scss b/packages/esm-commons-lib/src/root.scss index 62b18e4f5..a3f7b89ff 100644 --- a/packages/esm-commons-lib/src/root.scss +++ b/packages/esm-commons-lib/src/root.scss @@ -97,3 +97,20 @@ div[class*='-esm-login__styles__center'] > img { :global(.cds--overflow-menu) > div { width: 15rem !important; } + +:global(.cds--tab-content) { + background-color: #F4F4F4; +} + +:global(.cds--side-nav--expanded .cds--side-nav__items) { + padding: 0.125rem !important; +} + +:global(.cds--side-nav__submenu) { + height: 1.5rem; + + &:focus, + &:hover { + background-color: #e0e0e0; + } +} \ No newline at end of file diff --git a/packages/esm-commons-lib/src/utils/createNewOHRIDashboardLink.tsx b/packages/esm-commons-lib/src/utils/createNewOHRIDashboardLink.tsx new file mode 100644 index 000000000..08316ba86 --- /dev/null +++ b/packages/esm-commons-lib/src/utils/createNewOHRIDashboardLink.tsx @@ -0,0 +1,44 @@ +import React, { useEffect, useMemo, useState } from 'react'; +import classNames from 'classnames'; +import { BrowserRouter, useLocation } from 'react-router-dom'; +import { useTranslation } from 'react-i18next'; +import { ConfigurableLink, navigate } from '@openmrs/esm-framework'; +import { SideNavLink, SideNavMenu, SideNavMenuItem, Button } from '@carbon/react'; +import styles from './sidenav-links.scss'; + +interface DashboardLinkConfig { + name: string; + title: string; + icon: React.ComponentType; +} + +function NewDashboardExtension({ dashboardLinkConfig }: { dashboardLinkConfig: DashboardLinkConfig }) { + const { name, title, icon } = dashboardLinkConfig; + const location = useLocation(); + const spaBasePath = `${window.spaBase}/home`; + + const navLink = useMemo(() => { + const pathArray = location.pathname.split('/home'); + const lastElement = pathArray[pathArray.length - 1]; + return decodeURIComponent(lastElement); + }, [location.pathname]); + + return ( + { + e.preventDefault(); + navigate({ to: `${spaBasePath}/${name}` }); + }} + className={navLink.match(name) ? styles.currentNavItem : ''}> + {title} + + ); +} + +export const createNewOHRIDashboardLink = (dashboardLinkConfig: DashboardLinkConfig) => () => ( + + + +); diff --git a/packages/esm-commons-lib/src/utils/createOHRIGroupedLink.tsx b/packages/esm-commons-lib/src/utils/createOHRIGroupedLink.tsx new file mode 100644 index 000000000..c802ba29c --- /dev/null +++ b/packages/esm-commons-lib/src/utils/createOHRIGroupedLink.tsx @@ -0,0 +1,78 @@ +import React, { useMemo } from 'react'; +import classNames from 'classnames'; +import { BrowserRouter, useLocation } from 'react-router-dom'; +import { useTranslation } from 'react-i18next'; +import { ConfigurableLink } from '@openmrs/esm-framework'; +import { SideNavMenu, SideNavItems } from '@carbon/react'; + +interface DashboardLinkConfig { + name: string; + title: string; + isFolder: boolean; + folderTitle: string; + folderIcon: React.ComponentType; + childLinks?: Array; + isHidden?: boolean; +} + +function DashboardExtension({ dashboardLinkConfig }: { dashboardLinkConfig: DashboardLinkConfig }) { + const { t } = useTranslation(); + const { name, title, isFolder, folderTitle, folderIcon, childLinks, isHidden } = dashboardLinkConfig; + const location = useLocation(); + const spaBasePath = `${window.spaBase}/home`; + + const navLink = useMemo(() => { + const pathArray = location.pathname.split('/home'); + const lastElement = pathArray[pathArray.length - 1]; + return decodeURIComponent(lastElement); + }, [location.pathname]); + + if (isHidden) { + return; + } + + if (isFolder) { + return ( + + + + {t(title)} + + {Array.isArray(childLinks) && + childLinks.map((childLink, childIndex) => ( + + {t(childLink.title)} + + ))} + + + ); + } else { + return ( + + + {t(title)} + + + ); + } +} + +export const createOHRIGroupedLink = (dashboardLinkConfig: DashboardLinkConfig) => () => ( + + + +); diff --git a/packages/esm-commons-lib/src/utils/ohri-forms-commons.ts b/packages/esm-commons-lib/src/utils/ohri-forms-commons.ts index 33efde274..c407722c0 100644 --- a/packages/esm-commons-lib/src/utils/ohri-forms-commons.ts +++ b/packages/esm-commons-lib/src/utils/ohri-forms-commons.ts @@ -1,8 +1,8 @@ -import { OHRIFormSchema, SessionMode } from '@openmrs/openmrs-form-engine-lib'; +import { FormSchema, SessionMode } from '@openmrs/openmrs-form-engine-lib'; import { launchOHRIWorkSpace } from '../workspace/ohri-workspace-utils'; export const launchForm = ( - form: OHRIFormSchema, + form: FormSchema, mode: SessionMode = 'enter', moduleName: string, title?: string, diff --git a/packages/esm-commons-lib/src/utils/sidenav-links.scss b/packages/esm-commons-lib/src/utils/sidenav-links.scss index 76c3df410..d1a06f8e3 100644 --- a/packages/esm-commons-lib/src/utils/sidenav-links.scss +++ b/packages/esm-commons-lib/src/utils/sidenav-links.scss @@ -8,4 +8,4 @@ background-color: #e0e0e0 !important; color: #161616 !important; border-left-color: var(--brand-01) !important; -} +} \ No newline at end of file diff --git a/packages/esm-commons-lib/src/workspace/ohri-workspace-utils.ts b/packages/esm-commons-lib/src/workspace/ohri-workspace-utils.ts index 9c797dc52..53bd3f150 100644 --- a/packages/esm-commons-lib/src/workspace/ohri-workspace-utils.ts +++ b/packages/esm-commons-lib/src/workspace/ohri-workspace-utils.ts @@ -1,7 +1,7 @@ import { getSyncLifecycle } from '@openmrs/esm-framework'; import { BehaviorSubject } from 'rxjs'; import { closeWorkspace, launchPatientWorkspace, registerWorkspace } from '@openmrs/esm-patient-common-lib'; -import { OHRIForm, SessionMode } from '@openmrs/openmrs-form-engine-lib'; +import { FormEngine, SessionMode } from '@openmrs/openmrs-form-engine-lib'; export interface WorkspaceContextProps { title: string; encounterUuid?: string; @@ -20,7 +20,7 @@ export const launchOHRIWorkSpace = (props: WorkspaceContextProps) => { const workspaceName = props.workspaceName || 'ohri-forms-' + counter++; const close = () => { - return closeWorkspace(workspaceName, true); + return closeWorkspace(workspaceName, { ignoreChanges: true }); }; const onFormSubmit = () => { props.state?.updateParent?.(); @@ -30,7 +30,7 @@ export const launchOHRIWorkSpace = (props: WorkspaceContextProps) => { name: workspaceName, title: props.title, preferredWindowSize: props.screenSize, - load: getSyncLifecycle(OHRIForm, { + load: getSyncLifecycle(FormEngine, { featureName: 'ohri-forms-workspace-item', moduleName: props.moduleName, }), diff --git a/packages/esm-covid-app/package.json b/packages/esm-covid-app/package.json index 8763ab5cf..53109d1fe 100644 --- a/packages/esm-covid-app/package.json +++ b/packages/esm-covid-app/package.json @@ -1,6 +1,6 @@ { "name": "@ohri/openmrs-esm-ohri-covid-app", - "version": "2.2.0", + "version": "2.3.3", "description": "COVID Microfrontend for OpenMRS HIV Reference Implementation (OHRI)", "browser": "dist/openmrs-esm-ohri-covid-app.js", "main": "src/index.ts", diff --git a/packages/esm-covid-app/src/config-schema.ts b/packages/esm-covid-app/src/config-schema.ts index 69f38ff8b..853afe612 100644 --- a/packages/esm-covid-app/src/config-schema.ts +++ b/packages/esm-covid-app/src/config-schema.ts @@ -97,6 +97,21 @@ export const configSchema = { CovidLabOrderFormName: 'COVID Lab Order Form', }, }, + formUuids: { + _type: Type.Object, + _description: 'Covid Form Uuids.', + _default: { + covidCaseFormUuid: 'c0fd71bd-37bc-3c8d-b2ff-149c0ff4d6f0', + covidAssessmentFormUuid: 'f5fb6bc4-6fc3-3462-a191-2fff0896bab3', + covidOutcomeFormUuid: '6515d898-439c-11ec-9103-238295f2dfd7', + covidVaccinationFormUuid: '3ae3a031-2e24-357d-81b3-205d5187090a', + covidLabTestFormUuid: 'e92fe922-4863-11ec-99cc-1fdd2d4e9d88', + covidLabResultFormUuid: 'cf8cd756-baef-38df-b84d-92968b42c113', + covidLabCancellationFormUuid: 'd38bc949-c95b-39eb-a2c5-08b82a36409c', + covidSampleCollectionFormUuid: '371d19b6-485f-11ec-99cc-1fdd2d4e9d88', + covidLabOrderFormUuid: 'f5fb6bc4-6fc3-3462-a191-2fff0896bab3', + }, + }, }; export interface ConfigObject { @@ -104,4 +119,5 @@ export interface ConfigObject { obsConcepts: Object; formNames: object; cohorts: object; + formUuids: Object; } diff --git a/packages/esm-covid-app/src/dashboard.meta.tsx b/packages/esm-covid-app/src/dashboard.meta.tsx index f382a6547..d88764052 100644 --- a/packages/esm-covid-app/src/dashboard.meta.tsx +++ b/packages/esm-covid-app/src/dashboard.meta.tsx @@ -42,7 +42,8 @@ export const covidClinicalViewDashboardMeta = { export const covid19CasesDashboardMeta = { name: 'covid-cases', slot: 'covid-cases-dashboard-slot', - config: { columns: 1, type: 'grid', programme: 'covid', dashboardTitle: 'COVID-19 Cases', icon: Coronavirus }, title: 'COVID-19 Cases', - dashboardIcon: Coronavirus, + isFolder: true, + folderTitle: 'COVID', + folderIcon: Coronavirus, }; diff --git a/packages/esm-covid-app/src/home.component.tsx b/packages/esm-covid-app/src/home.component.tsx new file mode 100644 index 000000000..24580fe77 --- /dev/null +++ b/packages/esm-covid-app/src/home.component.tsx @@ -0,0 +1,16 @@ +import { OHRIWelcomeSection } from '@ohri/openmrs-esm-ohri-commons-lib'; +import React from 'react'; +import CovidHomePatientTabs from './views/dashboard/patient-list-tabs/covid-patient-list-tabs.component'; +import CovidSummaryTiles from './views/dashboard/summary-tiles/covid-summary-tiles.component'; + +const Homecomponent = () => { + return ( +
+ + + +
+ ); +}; + +export default Homecomponent; diff --git a/packages/esm-covid-app/src/index.ts b/packages/esm-covid-app/src/index.ts index af05108d3..1e3795dba 100644 --- a/packages/esm-covid-app/src/index.ts +++ b/packages/esm-covid-app/src/index.ts @@ -1,7 +1,7 @@ import { defineConfigSchema, getAsyncLifecycle, getSyncLifecycle } from '@openmrs/esm-framework'; -import CovidVaccinations from './views/covid-vaccinations.encounter-list'; -import CovidLabResults from './views/lab-results.encounter-list'; -import CovidAssessment from './views/case-assessment.encounter-lists'; +import CovidVaccinations from './views/covid-vaccinations.component'; +import CovidLabResults from './views/lab-results.component'; +import CovidAssessment from './views/case-assessment.component'; import CovidHomePatientTabs from './views/dashboard/patient-list-tabs/covid-patient-list-tabs.component'; import CovidSummaryTiles from './views/dashboard/summary-tiles/covid-summary-tiles.component'; import { @@ -12,9 +12,10 @@ import { covid19CasesDashboardMeta, covidPatientChartMeta, } from './dashboard.meta'; -import { createOHRIDashboardLink, OHRIHome, OHRIWelcomeSection } from '@ohri/openmrs-esm-ohri-commons-lib'; +import { createOHRIDashboardLink, createOHRIGroupedLink, OHRIHome, OHRIWelcomeSection } from '@ohri/openmrs-esm-ohri-commons-lib'; import { createDashboardGroup, createDashboardLink } from '@openmrs/esm-patient-common-lib'; import { configSchema } from './config-schema'; +import rootComponent from './root.component'; export const importTranslation = require.context('../translations', false, /.json$/, 'lazy'); @@ -80,8 +81,5 @@ export const covidClinicalViewDashboardLink = getSyncLifecycle( createOHRIDashboardLink(covidClinicalViewDashboardMeta), options, ); -export const covidCasesDashboardLink = getSyncLifecycle(createOHRIDashboardLink(covid19CasesDashboardMeta), options); -export const covidCasesDashboard = getSyncLifecycle(OHRIHome, { - featureName: 'covid cases dashboard', - moduleName, -}); +export const covidCasesDashboardLink = getSyncLifecycle(createOHRIGroupedLink(covid19CasesDashboardMeta), options); +export const covidCasesDashboard = getSyncLifecycle(rootComponent, options); diff --git a/packages/esm-covid-app/src/root.component.tsx b/packages/esm-covid-app/src/root.component.tsx new file mode 100644 index 000000000..74d2066b4 --- /dev/null +++ b/packages/esm-covid-app/src/root.component.tsx @@ -0,0 +1,27 @@ +import React from 'react'; +import { SWRConfig } from 'swr'; +import { BrowserRouter, Route, Routes } from 'react-router-dom'; +import Home from './home.component'; + +const swrConfiguration = { + // Maximum number of retries when the backend returns an error + errorRetryCount: 3, +}; + +const Root: React.FC = () => { + const covidBasename = window.getOpenmrsSpaBase() + 'home/covid-cases'; + + return ( +
+ + + + } /> + + + +
+ ); +}; + +export default Root; diff --git a/packages/esm-covid-app/src/routes.json b/packages/esm-covid-app/src/routes.json index 9e423c8dd..e6dbba5f7 100644 --- a/packages/esm-covid-app/src/routes.json +++ b/packages/esm-covid-app/src/routes.json @@ -16,38 +16,6 @@ "title": "COVID" } }, - { - "name": "covid-cases-dashboard-ext", - "slot": "ohri-covid-dashboard-slot", - "component": "covidCasesDashboardLink", - "meta": { - "name": "covid-cases", - "slot": "covid-cases-dashboard-slot", - "config": { - "columns": 1, - "type": "grid", - "programme": "covid", - "dashboardTitle": "COVID-19 Cases" - }, - "path": "COVID-19", - "title": "COVID-19 Cases" - } - }, - { - "name": "covid-cases-dashboard", - "slot": "covid-cases-dashboard-slot", - "component": "covidCasesDashboard", - "meta": { - "name": "covid-cases", - "slot": "covid-cases-dashboard-slot", - "config": { - "columns": 1, - "programme": "covid", - "dashboardTitle": "COVID-19 Home Page" - }, - "title": "COVID-19 Cases" - } - }, { "name": "covid-home-header-ext", "slot": "covid-home-header-slot", @@ -115,6 +83,22 @@ "name": "covid-vaccinations-ext", "slot": "covid-vaccinations-dashboard-slot", "component": "covidVaccinationsDashboard" + }, + { + "name": "covid-cases-dashboard-ext", + "slot": "homepage-dashboard-slot", + "component": "covidCasesDashboardLink", + "meta": { + "name": "covid-cases", + "slot": "covid-cases-dashboard-slot", + "title": "TB Treatment" + }, + "order": 5 + }, + { + "name": "covid-cases-dashboard", + "slot": "covid-cases-dashboard-slot", + "component": "covidCasesDashboard" } ] } diff --git a/packages/esm-covid-app/src/views/case-assessment.encounter-lists.tsx b/packages/esm-covid-app/src/views/case-assessment.component.tsx similarity index 88% rename from packages/esm-covid-app/src/views/case-assessment.encounter-lists.tsx rename to packages/esm-covid-app/src/views/case-assessment.component.tsx index 2ad5d718e..e2be890ba 100644 --- a/packages/esm-covid-app/src/views/case-assessment.encounter-lists.tsx +++ b/packages/esm-covid-app/src/views/case-assessment.component.tsx @@ -17,6 +17,8 @@ const CovidAssessment: React.FC = ({ patientUuid }) const { t } = useTranslation(); const config = useConfig(); + const { covidAssessmentFormUuid, covidCaseFormUuid, covidOutcomeFormUuid } = config.formUuids; + const columns: EncounterListColumn[] = useMemo( () => [ { @@ -104,9 +106,17 @@ const CovidAssessment: React.FC = ({ patientUuid }) patientUuid={patientUuid} encounterType={config.encounterTypes.covid_Assessment_EncounterUUID} formList={[ - { name: config.formNames.CovidAssessmentFormName, excludedIntents: ['COVID_LAB_ASSESSMENT_EMBED'] }, - { name: config.formNames.CovidCaseFormName }, - { name: config.formNames.CovidOutcomeFormName, excludedIntents: ['COVID_OUTCOME_EMBED', '*'] }, + { + name: config.formNames.CovidAssessmentFormName, + excludedIntents: ['COVID_LAB_ASSESSMENT_EMBED'], + uuid: covidAssessmentFormUuid, + }, + { name: config.formNames.CovidCaseFormName, uuid: covidCaseFormUuid }, + { + name: config.formNames.CovidOutcomeFormName, + excludedIntents: ['COVID_OUTCOME_EMBED', '*'], + uuid: covidOutcomeFormUuid, + }, ]} columns={columns} description={displayText} diff --git a/packages/esm-covid-app/src/views/covid-outcomes.encounter-list.tsx b/packages/esm-covid-app/src/views/covid-outcomes.component.tsx similarity index 94% rename from packages/esm-covid-app/src/views/covid-outcomes.encounter-list.tsx rename to packages/esm-covid-app/src/views/covid-outcomes.component.tsx index bc3f7d38a..1a48cbe90 100644 --- a/packages/esm-covid-app/src/views/covid-outcomes.encounter-list.tsx +++ b/packages/esm-covid-app/src/views/covid-outcomes.component.tsx @@ -19,6 +19,8 @@ const CovidOutcomes: React.FC = ({ patientUuid }) => { const { t } = useTranslation(); const config = useConfig(); + const { covidCaseFormUuid } = config.formUuids; + const columns: EncounterListColumn[] = useMemo( () => [ { @@ -69,7 +71,7 @@ const CovidOutcomes: React.FC = ({ patientUuid }) => { = ({ patientUuid const { t } = useTranslation(); const config = useConfig(); + const { covidVaccinationFormUuid } = config.formUuids; + const columns: EncounterListColumn[] = useMemo( () => [ //TODO: Add Vaccination Status concept @@ -98,7 +100,7 @@ const CovidVaccinations: React.FC = ({ patientUuid = ({ patientUuid }) => { const { t } = useTranslation(); const config = useConfig(); + const { + covidLabOrderFormUuid, + covidLabResultFormUuid, + covidLabTestFormUuid, + covidSampleCollectionFormUuid, + covidLabCancellationFormUuid, + } = config.formUuids; + const columnsLab: EncounterListColumn[] = useMemo( () => [ { @@ -223,16 +231,26 @@ const CovidLabResults: React.FC = ({ patientUuid }) => { patientUuid={patientUuid} encounterType={config.encounterTypes.covidLabOrderEncounterType_UUID} formList={[ - { name: config.formNames.CovidLabOrderFormName, excludedIntents: ['COVID_LAB_ORDER_EMBED'] }, - { name: config.formNames.CovidLabResultFormName, excludedIntents: ['COVID_LAB_RESULT_EMBED'] }, - { name: config.formNames.CovidLabTestFormName, excludedIntents: ['*'] }, + { + name: config.formNames.CovidLabOrderFormName, + excludedIntents: ['COVID_LAB_ORDER_EMBED'], + uuid: covidLabOrderFormUuid, + }, + { + name: config.formNames.CovidLabResultFormName, + excludedIntents: ['COVID_LAB_RESULT_EMBED'], + uuid: covidLabResultFormUuid, + }, + { name: config.formNames.CovidLabTestFormName, excludedIntents: ['*'], uuid: covidLabTestFormUuid }, { name: config.formNames.CovidLabCancellationFormName, excludedIntents: ['*', 'COVID_LAB_CANCELLATION_EMBED'], + uuid: covidLabCancellationFormUuid, }, { name: config.formNames.CovidSampleCollectionFormName, excludedIntents: ['*', 'COVID_SAMPLE_COLLECTION_EMBED'], + uuid: covidSampleCollectionFormUuid, }, ]} columns={columnsLab} @@ -249,12 +267,12 @@ const CovidLabResults: React.FC = ({ patientUuid }) => { patientUuid={patientUuid} encounterType={config.encounterTypes.covidLabOrderEncounterType_UUID} formList={[ - { name: config.formNames.CovidLabTestFormName }, - { name: config.formNames.CovidLabTestFormName }, - { name: config.formNames.CovidLabResultFormName }, - { name: config.formNames.CovidLabCancellationFormName }, - { name: config.formNames.CovidSampleCollectionFormName }, - { name: config.formNames.CovidLabOrderFormName }, + { name: config.formNames.CovidLabTestFormName, uuid: covidLabTestFormUuid }, + { name: config.formNames.CovidLabTestFormName, uuid: covidLabTestFormUuid }, + { name: config.formNames.CovidLabResultFormName, uuid: covidLabResultFormUuid }, + { name: config.formNames.CovidLabCancellationFormName, uuid: covidLabCancellationFormUuid }, + { name: config.formNames.CovidSampleCollectionFormName, uuid: covidSampleCollectionFormUuid }, + { name: config.formNames.CovidLabOrderFormName, uuid: covidLabOrderFormUuid }, ]} columns={columnsPending} description={headerTitlePending} diff --git a/packages/esm-form-render-app/package.json b/packages/esm-form-render-app/package.json index e9db3380f..b35e6f1dd 100644 --- a/packages/esm-form-render-app/package.json +++ b/packages/esm-form-render-app/package.json @@ -1,6 +1,6 @@ { "name": "@ohri/openmrs-esm-ohri-form-render-app", - "version": "2.2.0", + "version": "2.3.3", "description": "A Forms Rendering microfrontend for OpenMRS HIV Reference Implementation (OHRI)", "browser": "dist/openmrs-esm-ohri-form-render-app.js", "main": "src/index.ts", @@ -39,7 +39,7 @@ }, "dependencies": { "@carbon/react": "^1.13.0", - "@openmrs/openmrs-form-engine-lib": "next", + "@openmrs/openmrs-form-engine-lib": "1.1.0-pre.723", "ace-builds": "^1.4.12", "react-ace": "^9.4.4" }, diff --git a/packages/esm-form-render-app/src/render/forms-render-test.component.tsx b/packages/esm-form-render-app/src/render/forms-render-test.component.tsx index efe4ed91e..1fc3442ba 100644 --- a/packages/esm-form-render-app/src/render/forms-render-test.component.tsx +++ b/packages/esm-form-render-app/src/render/forms-render-test.component.tsx @@ -4,7 +4,7 @@ import styles from './form-render.scss'; import { Run, Maximize, UserData } from '@carbon/react/icons'; import AceEditor from 'react-ace'; import 'ace-builds/webpack-resolver'; -import { applyFormIntent, loadSubforms, OHRIForm, OHRIFormSchema } from '@openmrs/openmrs-form-engine-lib'; +import { applyFormIntent, loadSubforms, FormEngine, FormSchema } from '@openmrs/openmrs-form-engine-lib'; import { useTranslation } from 'react-i18next'; import { ConfigObject, useConfig, openmrsFetch } from '@openmrs/esm-framework'; import { handleFormValidation } from '../form-validator'; @@ -13,7 +13,7 @@ function FormRenderTest() { const { t } = useTranslation(); const headerTitle = t('formRenderTestTitle', 'Form Render Test'); const { patientUuid, dataTypeToRenderingMap } = useConfig() as ConfigObject; - const [formInput, setFormInput] = useState(); + const [formInput, setFormInput] = useState(); const [formIntents, setFormIntents] = useState([]); const [isIntentsDropdownDisabled, setIsIntentsDropdownDisabled] = useState(true); const [selectedFormIntent, setSelectedFormIntent] = useState(''); @@ -76,7 +76,7 @@ function FormRenderTest() { }; const formValidation = () => { - handleFormValidation(schemaInput, dataTypeToRenderingMap).then((response) => console.log(response)); + handleFormValidation(schemaInput, dataTypeToRenderingMap); }; const handleFormSubmission = (e) => { @@ -267,7 +267,7 @@ function FormRenderTest() { {isSchemaLoaded ? (
- { + return ( +
+ + + +
+ ); +}; + +export default Homecomponent; diff --git a/packages/esm-hiv-app/src/hiv-treatment-root.component.tsx b/packages/esm-hiv-app/src/hiv-treatment-root.component.tsx new file mode 100644 index 000000000..481100308 --- /dev/null +++ b/packages/esm-hiv-app/src/hiv-treatment-root.component.tsx @@ -0,0 +1,27 @@ +import React from 'react'; +import { SWRConfig } from 'swr'; +import { BrowserRouter, Route, Routes } from 'react-router-dom'; +import Home from './hiv-treatment-home.component'; + +const swrConfiguration = { + // Maximum number of retries when the backend returns an error + errorRetryCount: 3, +}; + +const Root: React.FC = () => { + const hivTreatmentBasename = window.getOpenmrsSpaBase() + 'home/care-and-treatment'; + + return ( +
+ + + + } /> + + + +
+ ); +}; + +export default Root; diff --git a/packages/esm-hiv-app/src/hts-home.component.tsx b/packages/esm-hiv-app/src/hts-home.component.tsx new file mode 100644 index 000000000..9390fc97a --- /dev/null +++ b/packages/esm-hiv-app/src/hts-home.component.tsx @@ -0,0 +1,16 @@ +import { OHRIWelcomeSection } from '@ohri/openmrs-esm-ohri-commons-lib'; +import React from 'react'; +import OHRIPatientTabs from './views/hts/home/patient-tabs/ohri-patient-tabs.component'; +import HTSSummaryTiles from './views/hts/home/summary-tiles/hts-summary-tiles.component'; + +const Homecomponent = () => { + return ( +
+ + {}} /> + +
+ ); +}; + +export default Homecomponent; diff --git a/packages/esm-hiv-app/src/hts-root.component.tsx b/packages/esm-hiv-app/src/hts-root.component.tsx new file mode 100644 index 000000000..f709f26bb --- /dev/null +++ b/packages/esm-hiv-app/src/hts-root.component.tsx @@ -0,0 +1,27 @@ +import React from 'react'; +import { SWRConfig } from 'swr'; +import { BrowserRouter, Route, Routes } from 'react-router-dom'; +import Home from './hts-home.component'; + +const swrConfiguration = { + // Maximum number of retries when the backend returns an error + errorRetryCount: 3, +}; + +const Root: React.FC = () => { + const htsBasename = window.getOpenmrsSpaBase() + 'home/hts'; + + return ( +
+ + + + } /> + + + +
+ ); +}; + +export default Root; diff --git a/packages/esm-hiv-app/src/index.ts b/packages/esm-hiv-app/src/index.ts index 884eff4ee..68bc900ea 100644 --- a/packages/esm-hiv-app/src/index.ts +++ b/packages/esm-hiv-app/src/index.ts @@ -16,6 +16,7 @@ import { PatientStatusBannerTag, OHRIHome, OHRIWelcomeSection, + createOHRIGroupedLink, } from '@ohri/openmrs-esm-ohri-commons-lib'; import { createDashboardGroup, createDashboardLink } from '@openmrs/esm-patient-common-lib'; @@ -33,6 +34,8 @@ import { partnerNotificationServicesDashboardMeta, hivPreventionFolderDashboardMeta, } from './dashboard.meta'; +import htsRootComponent from './hts-root.component'; +import careAndTreatmentRootComponent from './hiv-treatment-root.component'; import { configSchema } from './config-schema'; @@ -93,23 +96,20 @@ export const hivCareAndTreatmentFolderLink = getSyncLifecycle( options, ); export const hivCareAndTreatmentDashboardLink = getSyncLifecycle( - createOHRIDashboardLink(careAndTreatmentDashboardMeta), + createOHRIGroupedLink(hivCareAndTreatmentFolderDashboardMeta), options, ); -export const hivCareAndTreatmentDashboard = getSyncLifecycle(OHRIHome, { - featureName: 'care and treatment dashboard', - moduleName, -}); +export const hivCareAndTreatmentDashboard = getSyncLifecycle(careAndTreatmentRootComponent, options); export const hivPreventionFolderLink = getSyncLifecycle( createOHRIDashboardLink(hivPreventionFolderDashboardMeta), options, ); -export const hivPreventionDashboardLink = getSyncLifecycle(createOHRIDashboardLink(htsDashboardMeta), options); -export const hivPreventionDashboard = getSyncLifecycle(OHRIHome, { - featureName: 'hts dashboard', - moduleName, -}); +export const hivPreventionDashboardLink = getSyncLifecycle( + createOHRIGroupedLink(hivPreventionFolderDashboardMeta), + options, +); +export const hivPreventionDashboard = getSyncLifecycle(htsRootComponent, options); // Lab Results export const labResultsHomeHeader = getSyncLifecycle(OHRIWelcomeSection, { diff --git a/packages/esm-hiv-app/src/routes.json b/packages/esm-hiv-app/src/routes.json index de547b7ed..98a109545 100644 --- a/packages/esm-hiv-app/src/routes.json +++ b/packages/esm-hiv-app/src/routes.json @@ -23,35 +23,19 @@ }, { "name": "ohri-hiv-prevention-dashboard-ext", - "slot": "ohri-hiv-prevention-dashboard-slot", + "slot": "homepage-dashboard-slot", "component": "hivPreventionDashboardLink", "meta": { "name": "hts", "slot": "hts-dashboard-slot", - "config": { - "columns": 1, - "type": "grid", - "programme": "hts", - "dashboardTitle": "HTS Home Page" - }, "title": "HIV Testing Services" - } + }, + "order": 8 }, { "name": "ohri-hiv-prevention-dashboard", "slot": "hts-dashboard-slot", - "component": "hivPreventionDashboard", - "meta": { - "name": "hts", - "slot": "hts-dashboard-slot", - "config": { - "columns": 1, - "type": "grid", - "programme": "hts", - "dashboardTitle": "HTS Home Page" - }, - "title": "HIV Testing Services" - } + "component": "hivPreventionDashboard" }, { "name": "hts-home-header-ext", @@ -80,36 +64,19 @@ }, { "name": "care-and-treatment-dashboard-ext", - "slot": "ohri-hiv-care-and-treatment-dashboard-slot", + "slot": "homepage-dashboard-slot", "component": "hivCareAndTreatmentDashboardLink", "meta": { "name": "care-and-treatment", "slot": "ct-dashboard-slot", - "config": { - "columns": 1, - "type": "grid", - "programme": "ct", - "dashboardTitle": "C&T Home Page" - }, - "title": "Care and Treatment", - "path": "Care and Treatment" - } + "title": "Care and Treatment" + }, + "order": 9 }, { "name": "ohri-care-and-treatment-dashboard", "slot": "ct-dashboard-slot", - "component": "hivCareAndTreatmentDashboard", - "meta": { - "name": "care-and-treatment", - "slot": "ct-dashboard-slot", - "config": { - "columns": 1, - "type": "grid", - "programme": "ct", - "dashboardTitle": "C&T Home Page" - }, - "title": "Care and Treatment" - } + "component": "hivCareAndTreatmentDashboard" }, { "name": "ct-home-header-ext", diff --git a/packages/esm-hiv-app/src/views/clinical-visit/encounter-list/clinical-visit-encounter-list.component.tsx b/packages/esm-hiv-app/src/views/clinical-visit/encounter-list/clinical-visit-encounter-list.component.tsx index 25e26dac2..3b3aaa6a0 100644 --- a/packages/esm-hiv-app/src/views/clinical-visit/encounter-list/clinical-visit-encounter-list.component.tsx +++ b/packages/esm-hiv-app/src/views/clinical-visit/encounter-list/clinical-visit-encounter-list.component.tsx @@ -11,7 +11,7 @@ interface ClinicalVisitWidgetProps { const ClinicalVisitWidget: React.FC = ({ patientUuid }) => { const { t } = useTranslation(); - const { obsConcepts, encounterTypes } = useConfig(); + const { obsConcepts, encounterTypes, formNames, formUuids } = useConfig(); const columns: EncounterListColumn[] = useMemo( () => [ @@ -70,7 +70,7 @@ const ClinicalVisitWidget: React.FC = ({ patientUuid } = ({ patientUuid }) => { const { t } = useTranslation(); - const { obsConcepts, encounterTypes, formNames } = useConfig(); + const { obsConcepts, encounterTypes, formNames, formUuids } = useConfig(); const columns: EncounterListColumn[] = useMemo( () => [ @@ -67,7 +67,7 @@ const DisclosureList: React.FC = ({ patientUuid }) => { patientUuid={patientUuid} filter={disclosureFilter} encounterType={encounterTypes.PeadsDisclosureEncounterType_UUID} - formList={[{ name: formNames.DisclosureFormName }]} + formList={[{ name: formNames.DisclosureFormName, uuid: formUuids.disclosureFormUuid }]} columns={columns} description={headerTitle} headerTitle={headerTitle} diff --git a/packages/esm-hiv-app/src/views/general-counselling/tabs/intimate-partner-violence.component.tsx b/packages/esm-hiv-app/src/views/general-counselling/tabs/intimate-partner-violence.component.tsx index 27c494b11..53d06b8a2 100644 --- a/packages/esm-hiv-app/src/views/general-counselling/tabs/intimate-partner-violence.component.tsx +++ b/packages/esm-hiv-app/src/views/general-counselling/tabs/intimate-partner-violence.component.tsx @@ -10,7 +10,7 @@ interface IntimatePartnerViolenceListProps { const IntimatePartnerViolenceList: React.FC = ({ patientUuid }) => { const { t } = useTranslation(); - const { obsConcepts, encounterTypes, formNames } = useConfig(); + const { obsConcepts, encounterTypes, formNames, formUuids } = useConfig(); const columns: EncounterListColumn[] = useMemo( () => [ @@ -76,7 +76,7 @@ const IntimatePartnerViolenceList: React.FC = patientUuid={patientUuid} filter={intimatePartnerFilter} encounterType={encounterTypes.IntimatePartnerEncounterType_UUID} - formList={[{ name: formNames.IntimatePartnerFormName }]} + formList={[{ name: formNames.IntimatePartnerFormName, uuid: formUuids.intimatePartnerFormUuid }]} columns={columns} description={displayText} headerTitle={headerTitle} diff --git a/packages/esm-hiv-app/src/views/general-counselling/tabs/mental-health-assessment.component.tsx b/packages/esm-hiv-app/src/views/general-counselling/tabs/mental-health-assessment.component.tsx index f2bfbc214..c9afe0d35 100644 --- a/packages/esm-hiv-app/src/views/general-counselling/tabs/mental-health-assessment.component.tsx +++ b/packages/esm-hiv-app/src/views/general-counselling/tabs/mental-health-assessment.component.tsx @@ -11,7 +11,7 @@ interface MentalHealthAssessmentListProps { const MentalHealthAssessmentList: React.FC = ({ patientUuid }) => { const { t } = useTranslation(); - const { obsConcepts, encounterTypes, formNames } = useConfig(); + const { obsConcepts, encounterTypes, formNames, formUuids } = useConfig(); const columns: EncounterListColumn[] = useMemo( () => [ @@ -86,7 +86,7 @@ const MentalHealthAssessmentList: React.FC = ({ patientUuid={patientUuid} filter={mentalHealthFilter} encounterType={encounterTypes.MentalHealthAssessmentEncounter_UUID} - formList={[{ name: formNames.MentalHealthFormName }]} + formList={[{ name: formNames.MentalHealthFormName, uuid: formUuids.mentalHealthFormUuid }]} columns={columns} description={displayText} headerTitle={headerTitle} diff --git a/packages/esm-hiv-app/src/views/hiv-testing-services/tab-list/hts-overview-list.component.tsx b/packages/esm-hiv-app/src/views/hiv-testing-services/tab-list/hts-overview-list.component.tsx index c3dac3dac..f093916fe 100644 --- a/packages/esm-hiv-app/src/views/hiv-testing-services/tab-list/hts-overview-list.component.tsx +++ b/packages/esm-hiv-app/src/views/hiv-testing-services/tab-list/hts-overview-list.component.tsx @@ -14,7 +14,7 @@ const HtsOverviewList: React.FC = ({ patientUuid }) => { const htsRetrospectiveTypeUUID = '79c1f50f-f77d-42e2-ad2a-d29304dde2fe'; // HTS Retrospective const forceComponentUpdate = () => setCounter(counter + 1); const headerTitle = t('hivTesting', 'HIV Testing'); - const { obsConcepts, formNames } = useConfig(); + const { obsConcepts, formNames, formUuids } = useConfig(); const columns: EncounterListColumn[] = useMemo( () => [ @@ -83,6 +83,7 @@ const HtsOverviewList: React.FC = ({ patientUuid }) => { name: formNames.HIVTestingFormName, fixedIntent: '*', excludedIntents: ['HTS_PRE_TEST', 'HTS_TEST', 'HTS_POST_TEST'], + uuid: formUuids.hivTestingFormUuid, }, ]} columns={columns} diff --git a/packages/esm-hiv-app/src/views/hts/care-and-treatment/summary-tiles/ct-summary-tiles.component.tsx b/packages/esm-hiv-app/src/views/hts/care-and-treatment/summary-tiles/ct-summary-tiles.component.tsx index bb5cfbd83..6d180737e 100644 --- a/packages/esm-hiv-app/src/views/hts/care-and-treatment/summary-tiles/ct-summary-tiles.component.tsx +++ b/packages/esm-hiv-app/src/views/hts/care-and-treatment/summary-tiles/ct-summary-tiles.component.tsx @@ -3,7 +3,7 @@ import { useTranslation } from 'react-i18next'; import { OHRIProgrammeSummaryTiles, getReportingCohort } from '@ohri/openmrs-esm-ohri-commons-lib'; import { useConfig } from '@openmrs/esm-framework'; -function CTSummaryTiles({ launchWorkSpace }) { +function CTSummaryTiles() { const { t } = useTranslation(); const [activeClientsCount, setActiveClientsCount] = useState(0); const { cohorts } = useConfig(); diff --git a/packages/esm-hiv-app/src/views/hts/home/summary-tiles/summary-tile.scss b/packages/esm-hiv-app/src/views/hts/home/summary-tiles/summary-tile.scss index 46fc47ab8..b1e41454d 100644 --- a/packages/esm-hiv-app/src/views/hts/home/summary-tiles/summary-tile.scss +++ b/packages/esm-hiv-app/src/views/hts/home/summary-tiles/summary-tile.scss @@ -1,5 +1,6 @@ .desktopView{ display: flex; + margin: 1rem; } .tileView { display: none; diff --git a/packages/esm-hiv-app/src/views/hts/lab-results/tabs/cd4-results.component.tsx b/packages/esm-hiv-app/src/views/hts/lab-results/tabs/cd4-results.component.tsx index 0745de60d..8bf769660 100644 --- a/packages/esm-hiv-app/src/views/hts/lab-results/tabs/cd4-results.component.tsx +++ b/packages/esm-hiv-app/src/views/hts/lab-results/tabs/cd4-results.component.tsx @@ -61,9 +61,8 @@ const CD4ResultsList: React.FC = ({ patientUuid }) => { for (let patient of patients) { const lastCd4Result = patientToCd4Map.find((entry) => entry.patientId === patient.resource.id)?.cd4Result; const lastCd4ResultDate = patientToCd4Map.find((entry) => entry.patientId === patient.resource.id)?.cd4ResultDate; - const lastCd4EncounterUuid = patientToCd4Map.find( - (entry) => entry.patientId === patient.resource.id, - )?.cd4EncounterUuid; + const lastCd4EncounterUuid = patientToCd4Map.find((entry) => entry.patientId === patient.resource.id) + ?.cd4EncounterUuid; const patientActions = ( = () => { useEffect(() => { let rows = []; for (let patient of patients) { - const lastviralLoadResult = patientToViralLoadMap.find( - (entry) => entry.patientId === patient.resource.id, - )?.viralLoadResult; - const lastviralLoadResultDate = patientToViralLoadMap.find( - (entry) => entry.patientId === patient.resource.id, - )?.viralLoadResultDate; - const lastViralLoadEncounterUuid = patientToViralLoadMap.find( - (entry) => entry.patientId === patient.resource.id, - )?.viralEncounterUuid; + const lastviralLoadResult = patientToViralLoadMap.find((entry) => entry.patientId === patient.resource.id) + ?.viralLoadResult; + const lastviralLoadResultDate = patientToViralLoadMap.find((entry) => entry.patientId === patient.resource.id) + ?.viralLoadResultDate; + const lastViralLoadEncounterUuid = patientToViralLoadMap.find((entry) => entry.patientId === patient.resource.id) + ?.viralEncounterUuid; const patientActions = ( = ({ patientUuid }) => { const { t } = useTranslation(); const headerTitle = t('cd4', 'CD4'); - const { obsConcepts, encounterTypes, formNames } = useConfig(); + const { obsConcepts, encounterTypes, formNames, formUuids } = useConfig(); const columns: EncounterListColumn[] = useMemo( () => [ @@ -72,7 +72,7 @@ const CD4OverviewList: React.FC = ({ patientUuid }) => { patientUuid={patientUuid} filter={cd4LabResultsFilter} encounterType={encounterTypes.CD4LabResultsEncounter_UUID} - formList={[{ name: formNames.CD4LabResultsFormName }]} + formList={[{ name: formNames.CD4LabResultsFormName, uuid: formUuids.cd4LabResultsFormUuid }]} columns={columns} description={headerTitle} headerTitle={headerTitle} diff --git a/packages/esm-hiv-app/src/views/lab-results/encounter-list/lab-results-encounter-list.component.tsx b/packages/esm-hiv-app/src/views/lab-results/encounter-list/lab-results-encounter-list.component.tsx index 7d18fdeb0..ad0f4a410 100644 --- a/packages/esm-hiv-app/src/views/lab-results/encounter-list/lab-results-encounter-list.component.tsx +++ b/packages/esm-hiv-app/src/views/lab-results/encounter-list/lab-results-encounter-list.component.tsx @@ -15,7 +15,7 @@ interface LabResultsOverviewListProps { const LabResultsOverviewList: React.FC = ({ patientUuid }) => { const { t } = useTranslation(); - const { obsConcepts, encounterTypes, formNames } = useConfig(); + const { obsConcepts, encounterTypes, formNames, formUuids } = useConfig(); const columns: EncounterListColumn[] = useMemo( () => [ @@ -86,7 +86,7 @@ const LabResultsOverviewList: React.FC = ({ patient patientUuid={patientUuid} filter={viralLoadRequestFilter} encounterType={encounterTypes.ViralLoadResultsEncounter_UUID} - formList={[{ name: formNames.ViralLoadRequestFormName }]} + formList={[{ name: formNames.ViralLoadRequestFormName, uuid: formUuids.viralLoadRequestFormUuid }]} columns={columns} description={displayText} headerTitle={headerTitle} diff --git a/packages/esm-hiv-app/src/views/partner-notification-services/partner-notification.component.tsx b/packages/esm-hiv-app/src/views/partner-notification-services/partner-notification.component.tsx index 09dad0ccb..b330551e3 100644 --- a/packages/esm-hiv-app/src/views/partner-notification-services/partner-notification.component.tsx +++ b/packages/esm-hiv-app/src/views/partner-notification-services/partner-notification.component.tsx @@ -17,7 +17,7 @@ interface PartnerNotificationListProps { const PartnerNotificationList: React.FC = ({ patientUuid }) => { const { t } = useTranslation(); - const { obsConcepts, encounterTypes, formNames } = useConfig(); + const { obsConcepts, encounterTypes, formNames, formUuids } = useConfig(); const columns: EncounterListColumn[] = useMemo( () => [ @@ -97,7 +97,7 @@ const PartnerNotificationList: React.FC = ({ patie patientUuid={patientUuid} filter={partnerNotificationFilter} encounterType={encounterTypes.PatnerNotificationEncounterType_UUID} - formList={[{ name: formNames.PartnerNotificationFormName }]} + formList={[{ name: formNames.PartnerNotificationFormName, uuid: formUuids.partnerNotificationFormUuid }]} columns={columns} description={headerTitle} headerTitle={headerTitle} diff --git a/packages/esm-hiv-app/src/views/partner-notification-services/tabs/contact-tracing.component.tsx b/packages/esm-hiv-app/src/views/partner-notification-services/tabs/contact-tracing.component.tsx index 7f9b383ac..2bdf7df75 100644 --- a/packages/esm-hiv-app/src/views/partner-notification-services/tabs/contact-tracing.component.tsx +++ b/packages/esm-hiv-app/src/views/partner-notification-services/tabs/contact-tracing.component.tsx @@ -11,7 +11,7 @@ interface ContactTracingListProps { const ContactTracingList: React.FC = ({ patientUuid }) => { const { t } = useTranslation(); - const { obsConcepts, encounterTypes, formNames } = useConfig(); + const { obsConcepts, encounterTypes, formNames, formUuids } = useConfig(); const columnsLab: EncounterListColumn[] = useMemo( () => [ @@ -70,7 +70,7 @@ const ContactTracingList: React.FC = ({ patientUuid }) = ({ patientUuid }) => { const { t } = useTranslation(); - const { obsConcepts, encounterTypes, formNames } = useConfig(); + const { obsConcepts, encounterTypes, formNames, formUuids } = useConfig(); const columnsLab: EncounterListColumn[] = useMemo( () => [ @@ -70,7 +70,7 @@ const PatientTracingList: React.FC = ({ patientUuid }) = ({ patientUuid }) => { const { t } = useTranslation(); - const { obsConcepts, encounterTypes, formNames } = useConfig(); + const { obsConcepts, encounterTypes, formNames, formUuids } = useConfig(); const artConcepts = useMemo( () => @@ -189,7 +189,7 @@ const ArtTherapyTabList: React.FC = ({ patientUuid }) => = ({ patientUuid }) => { const { t } = useTranslation(); - const { obsConcepts, encounterTypes, formNames } = useConfig(); + const { obsConcepts, encounterTypes, formNames, formUuids } = useConfig(); const columnsLab: EncounterListColumn[] = useMemo( () => [ @@ -67,7 +67,7 @@ const DeathTabList: React.FC = ({ patientUuid }) => { = ({ patientUuid }) => { const { t } = useTranslation(); - const { obsConcepts, encounterTypes, formNames } = useConfig(); + const { obsConcepts, encounterTypes, formNames, formUuids } = useConfig(); const columns: EncounterListColumn[] = useMemo( () => [ @@ -95,7 +95,7 @@ const HIVEnrolmentTabList: React.FC = ({ patientUuid } = ({ patientUuid }) => { const { t } = useTranslation(); - const { obsConcepts, encounterTypes, formNames } = useConfig(); + const { obsConcepts, encounterTypes, formNames, formUuids } = useConfig(); const columns: EncounterListColumn[] = useMemo( () => [ @@ -70,7 +70,7 @@ const ServiceDeliveryTabList: React.FC = ({ patient = ({ patientUuid }) => { const { t } = useTranslation(); - const { obsConcepts, encounterTypes, formNames } = useConfig(); + const { obsConcepts, encounterTypes, formNames, formUuids } = useConfig(); const columnsLab: EncounterListColumn[] = useMemo( () => [ @@ -25,7 +25,11 @@ const TransferOutTabList: React.FC = ({ patientUuid }) key: 'visitDate', header: t('visitDate', 'Visit Date'), getValue: (encounter) => { - return getObsFromEncounter(encounter, obsConcepts.dateOfEncounterConcept, true); + const obsVisitDate = getObsFromEncounter(encounter, obsConcepts.dateOfEncounterConcept, true); + const encounterDate = encounter.encounterDatetime; + return obsVisitDate === '--' && encounterDate + ? formatDate(parseDate(encounterDate), { mode: 'wide' }) + : obsVisitDate; }, }, { @@ -85,7 +89,7 @@ const TransferOutTabList: React.FC = ({ patientUuid }) = ({ patientUuid }) => { const { t } = useTranslation(); - const { obsConcepts, encounterTypes, formNames } = useConfig(); + const { obsConcepts, encounterTypes, formNames, formUuids } = useConfig(); const columns: EncounterListColumn[] = useMemo( () => [ @@ -75,7 +75,7 @@ const ServiceEnrolmentWidget: React.FC = ({ patientUuid } = ({ patientUuid }) => { const { t } = useTranslation(); - const { obsConcepts, encounterTypes, formNames } = useConfig(); + const { obsConcepts, encounterTypes, formNames, formUuids } = useConfig(); const headerTitle = t('clinicalVisit', 'Clinical Visit'); const displayText = t('clinicalVisit', 'Clinical Visit'); @@ -82,7 +82,7 @@ const ClinicalVisitList: React.FC = ({ patientUuid }) => { return htsPages.map((htsPage) => ({ diff --git a/packages/esm-ohri-core-app/src/index.ts b/packages/esm-ohri-core-app/src/index.ts index 3a22766ec..01062df9e 100644 --- a/packages/esm-ohri-core-app/src/index.ts +++ b/packages/esm-ohri-core-app/src/index.ts @@ -2,16 +2,10 @@ import { defineConfigSchema, getAsyncLifecycle, getSyncLifecycle, provide } from import { createOHRIPatientChartSideNavLink, patientChartDivider_dashboardMeta, - createOHRIDashboardLink, PatientList, PatientTable, } from '@ohri/openmrs-esm-ohri-commons-lib'; -import { - appointmentsDashboardMeta, - homeDashboardMeta, - dispensingDashboardMeta, - serviceQueuesDashboardMeta, -} from './dashboard.meta'; +import ProgramsHome from './ohri-dashboard/programs-home.component'; export const importTranslation = require.context('../translations', false, /.json$/, 'lazy'); @@ -26,42 +20,6 @@ export function startupApp() { defineConfigSchema(moduleName, {}); } -export const dashboard = getAsyncLifecycle(() => import('./root'), options); - -export const homeDashboard = getSyncLifecycle(createOHRIDashboardLink(homeDashboardMeta), options); - -export const patientList = getSyncLifecycle(PatientList, { - featureName: 'home', - moduleName, -}); - -export const appointmentsLink = getSyncLifecycle(createOHRIDashboardLink(appointmentsDashboardMeta), options); -export const appointmentsDashboard = getAsyncLifecycle( - () => import('./ohri-dashboard/appointments/appointments-dashboard.component'), - { - featureName: 'appointments-dashboard', - moduleName, - }, -); - -export const dispensingLink = getSyncLifecycle(createOHRIDashboardLink(dispensingDashboardMeta), options); -export const dispensingDashboard = getAsyncLifecycle( - () => import('./ohri-dashboard/dispensing/dispensing-dashboard.component'), - { - featureName: 'dispensing-dashboard', - moduleName, - }, -); - -export const serviceQueuesLink = getSyncLifecycle(createOHRIDashboardLink(serviceQueuesDashboardMeta), options); -export const serviceQueuesDashboard = getAsyncLifecycle( - () => import('./ohri-dashboard/service-queues/service-queues-dashboard.component'), - { - featureName: 'service-queues-dashboard', - moduleName, - }, -); - export const ohriNavItems = getAsyncLifecycle( () => import('./ohri-dashboard/side-menu/ohri-dashboard-side-nav.component'), { @@ -76,3 +34,10 @@ export const ohriClinicalViewsDivider = getSyncLifecycle( ); export const patientTable = getSyncLifecycle(PatientTable, options); + +export const patientList = getSyncLifecycle(PatientList, { + featureName: 'home', + moduleName, +}); + +export const programsText = getSyncLifecycle(ProgramsHome ,options); diff --git a/packages/esm-ohri-core-app/src/ohri-dashboard/appointments/appointments-dashboard.component.tsx b/packages/esm-ohri-core-app/src/ohri-dashboard/appointments/appointments-dashboard.component.tsx deleted file mode 100644 index 7b4197a9e..000000000 --- a/packages/esm-ohri-core-app/src/ohri-dashboard/appointments/appointments-dashboard.component.tsx +++ /dev/null @@ -1,8 +0,0 @@ -import React, { useEffect } from 'react'; -import { attach, detach, ExtensionSlot } from '@openmrs/esm-framework'; - -const AppointmentsDashboard = () => { - return ; -}; - -export default AppointmentsDashboard; diff --git a/packages/esm-ohri-core-app/src/ohri-dashboard/dispensing/dispensing-dashboard.component.tsx b/packages/esm-ohri-core-app/src/ohri-dashboard/dispensing/dispensing-dashboard.component.tsx deleted file mode 100644 index 32f2bae7e..000000000 --- a/packages/esm-ohri-core-app/src/ohri-dashboard/dispensing/dispensing-dashboard.component.tsx +++ /dev/null @@ -1,13 +0,0 @@ -import React, { useEffect } from 'react'; -import { attach, detach, ExtensionSlot } from '@openmrs/esm-framework'; - -const DispensingDashboard = () => { - useEffect(() => { - attach('ohri-dashboard-dispensing-slot', 'dispensing-dashboard'); - return () => detach('ohri-dashboard-dispensing-slot', 'dispensing-dashboard'); - }, []); - - return ; -}; - -export default DispensingDashboard; diff --git a/packages/esm-ohri-core-app/src/ohri-dashboard/programs-home.component.tsx b/packages/esm-ohri-core-app/src/ohri-dashboard/programs-home.component.tsx new file mode 100644 index 000000000..a3257208d --- /dev/null +++ b/packages/esm-ohri-core-app/src/ohri-dashboard/programs-home.component.tsx @@ -0,0 +1,7 @@ +import React from 'react'; + +const ProgramsHome = () => { + return
Programmes
; +}; + +export default ProgramsHome; diff --git a/packages/esm-ohri-core-app/src/ohri-dashboard/service-queues/service-queues-dashboard.component.tsx b/packages/esm-ohri-core-app/src/ohri-dashboard/service-queues/service-queues-dashboard.component.tsx deleted file mode 100644 index 9ab87c647..000000000 --- a/packages/esm-ohri-core-app/src/ohri-dashboard/service-queues/service-queues-dashboard.component.tsx +++ /dev/null @@ -1,13 +0,0 @@ -import React, { useEffect } from 'react'; -import { attach, detach, ExtensionSlot } from '@openmrs/esm-framework'; - -const ServiceQueuesDashboard = () => { - useEffect(() => { - attach('ohri-dashboard-service-queues-slot', 'service-queues-dashboard'); - return () => detach('ohri-dashboard-service-queues-slot', 'service-queues-dashboard'); - }, []); - - return ; -}; - -export default ServiceQueuesDashboard; diff --git a/packages/esm-ohri-core-app/src/root.tsx b/packages/esm-ohri-core-app/src/root.tsx deleted file mode 100644 index 6c7d6ac3b..000000000 --- a/packages/esm-ohri-core-app/src/root.tsx +++ /dev/null @@ -1,15 +0,0 @@ -import React from 'react'; -import { BrowserRouter, Navigate, Route, Routes } from 'react-router-dom'; -import OHRIDashboard from './ohri-dashboard/ohri-dashboard.component'; - -export default function Root() { - return ( - - - } /> - } /> - } /> - - - ); -} diff --git a/packages/esm-ohri-core-app/src/routes.json b/packages/esm-ohri-core-app/src/routes.json index 30d19b17b..0831c732f 100644 --- a/packages/esm-ohri-core-app/src/routes.json +++ b/packages/esm-ohri-core-app/src/routes.json @@ -3,69 +3,8 @@ "backendDependencies": { "webservices.rest": "^2.24.0" }, - "pages": [ - { - "component": "dashboard", - "route": "dashboard" - }, - { - "component": "dashboard", - "route": "home" - } - ], + "pages": [], "extensions": [ - { - "name": "home-dashboard-ext", - "slot": "dashboard-links-slot", - "component": "homeDashboard", - "meta": { - "title": "Home", - "name": "home", - "slot": "ohri-home-dashboard-slot", - "isLink": true - } - }, - { - "name": "ohri-patient-list", - "slot": "ohri-home-dashboard-slot", - "component": "patientList" - }, - { - "name": "appointments-ohri-dashboard-ext", - "slot": "dashboard-links-slot", - "component": "appointmentsLink" - }, - { - "name": "ohri-appointments-dashboard", - "slot": "ohri-appointments-dashboard-slot", - "component": "appointmentsDashboard" - }, - { - "name": "dispensing-ohri-dashboard-ext", - "slot": "dashboard-links-slot", - "component": "dispensingLink", - "meta": { - "title": "Dispensing", - "name": "dispensing", - "slot": "ohri-dispensing-dashboard-slot", - "isLink": true - } - }, - { - "name": "ohri-dispensing-dashboard", - "slot": "ohri-dispensing-dashboard-slot", - "component": "dispensingDashboard" - }, - { - "name": "service-queues-ohri-dashboard-ext", - "slot": "dashboard-links-slot", - "component": "serviceQueuesLink" - }, - { - "name": "ohri-service-queues-dashboard", - "slot": "ohri-service-queues-dashboard-slot", - "component": "serviceQueuesDashboard" - }, { "name": "ohri-nav-items-ext", "slot": "ohri-nav-items-slot", @@ -80,6 +19,17 @@ { "name": "patient-table", "component": "patientTable" + }, + { + "name": "ohri-patient-list", + "slot": "ohri-home-dashboard-slot", + "component": "patientList" + }, + { + "component": "programsText", + "name": "ohri-home-programs-text", + "slot": "homepage-dashboard-slot" , + "order": 4 } ] } diff --git a/packages/esm-ohri-pmtct-app/package.json b/packages/esm-ohri-pmtct-app/package.json index 474ee974f..8d1515735 100644 --- a/packages/esm-ohri-pmtct-app/package.json +++ b/packages/esm-ohri-pmtct-app/package.json @@ -1,6 +1,6 @@ { "name": "@ohri/openmrs-esm-ohri-pmtct", - "version": "2.2.0", + "version": "2.3.3", "description": "PMTCT microfrontend for OpenMRS HIV Reference Implementation (OHRI)", "browser": "dist/ohri-pmtct-app.js", "main": "src/index.ts", diff --git a/packages/esm-ohri-pmtct-app/src/config-schema.ts b/packages/esm-ohri-pmtct-app/src/config-schema.ts index 621ed759a..3c7e055af 100644 --- a/packages/esm-ohri-pmtct-app/src/config-schema.ts +++ b/packages/esm-ohri-pmtct-app/src/config-schema.ts @@ -8,6 +8,14 @@ export const configSchema = { preferredIdentifierSource: '8549f706-7e85-4c1d-9424-217d50a2988b', }, }, + identifiersTypes: { + _type: Type.Object, + _description: 'Identifier types for PMTCT.', + _default: { + ptrackerIdentifierType: '4da0a3fe-e546-463f-81fa-084f098ff06c', + artUniqueNumberType: '9d6d1eec-2cd6-4637-a981-4a46b4b8b41f', + }, + }, formNames: { _type: Type.Object, _description: 'List of forms for PMTCT.', @@ -15,6 +23,17 @@ export const configSchema = { antenatal: 'Antenatal Form', labourAndDelivery: 'Labour & Delivery Form', motherPostnatal: 'Mother - Postnatal Form', + infantPostnatal: 'Infant - Postanal Form', + }, + }, + formUuids: { + _type: Type.Object, + _description: 'List of uuids for PMTCT forms.', + _default: { + antenatal: '5255a535-2acb-3f44-bd0a-3f80595dece1', + labourAndDelivery: '1e5614d6-5306-11e6-beb8-9e71128cae77', + motherPostnatal: 'e6b67aa4-6c59-4470-8ad5-b994efeda553', + infantPostnatal: '5022c5d7-ea45-47ce-bd65-1ba1d8ad2467', }, }, encounterTypes: { @@ -25,8 +44,6 @@ export const configSchema = { laborAndDelivery: '6dc5308d-27c9-4d49-b16f-2c5e3c759757', infantPostnatal: 'af1f1b24-d2e8-4282-b308-0bf79b365584', motherPostnatal: '269bcc7f-04f8-4ddc-883d-7a3a0d569aad', - PTrackerIdentifierType: '4da0a3fe-e546-463f-81fa-084f098ff06c', - artUniqueNumberType: '9d6d1eec-2cd6-4637-a981-4a46b4b8b41f', mchEncounterType: '12de5bc5-352e-4faf-9961-a2125085a75c', }, }, @@ -89,6 +106,7 @@ export const configSchema = { export interface ConfigObject { identifiers: Object; + identifiersTypes: Object; encounterTypes: Object; obsConcepts: Object; formNames: Object; diff --git a/packages/esm-ohri-pmtct-app/src/dashboard.meta.tsx b/packages/esm-ohri-pmtct-app/src/dashboard.meta.tsx index 9b24fad76..622ff5445 100644 --- a/packages/esm-ohri-pmtct-app/src/dashboard.meta.tsx +++ b/packages/esm-ohri-pmtct-app/src/dashboard.meta.tsx @@ -38,13 +38,6 @@ export const childVisitsDashboardMeta = { export const motherChildDashboardMeta = { name: 'mother-child-health', slot: 'mother-child-health-dashboard-slot', - config: { - columns: 1, - type: 'grid', - programme: 'pmtct', - dashboardTitle: 'Mother Child Health Home Page', - icon: PedestrianChild, - }, - isLink: true, title: 'Maternal & Child Health', + icon: PedestrianChild, }; diff --git a/packages/esm-ohri-pmtct-app/src/home.component.tsx b/packages/esm-ohri-pmtct-app/src/home.component.tsx new file mode 100644 index 000000000..7eb3a0666 --- /dev/null +++ b/packages/esm-ohri-pmtct-app/src/home.component.tsx @@ -0,0 +1,16 @@ +import React from 'react'; +import { OHRIWelcomeSection } from '@ohri/openmrs-esm-ohri-commons-lib'; +import MaternalChildSummaryTiles from './views/summary-tabs/maternal-child-summary-tiles.component'; +import LabResultsSummary from './views/summary-tabs/mother-child-summary-tabs.component'; + +const Home: React.FC = () => { + return ( +
+ + + +
+ ); +}; + +export default Home; diff --git a/packages/esm-ohri-pmtct-app/src/index.ts b/packages/esm-ohri-pmtct-app/src/index.ts index 3b2300081..7097e1e25 100644 --- a/packages/esm-ohri-pmtct-app/src/index.ts +++ b/packages/esm-ohri-pmtct-app/src/index.ts @@ -1,4 +1,4 @@ -import { defineConfigSchema, getSyncLifecycle } from '@openmrs/esm-framework'; +import { defineConfigSchema, getAsyncLifecycle, getSyncLifecycle } from '@openmrs/esm-framework'; import MaternalSummary from './views/mch-summary/mch-summary.component'; import MaternalHealthList from './views/maternal-health/maternal-health.component'; import ChildHealthList from './views/child-health/child-health.component'; @@ -11,17 +11,18 @@ import { motherChildDashboardMeta, mchFolderMeta, } from './dashboard.meta'; -import { createDashboardGroup, createDashboardLink } from '@openmrs/esm-patient-common-lib'; import { registerPostSubmissionAction, registerExpressionHelper } from '@openmrs/openmrs-form-engine-lib'; import { createConditionalDashboardLink, - createOHRIDashboardLink, + createNewOHRIDashboardLink, OHRIHome, OHRIWelcomeSection, createConditionalDashboardGroup, } from '@ohri/openmrs-esm-ohri-commons-lib'; import { generateInfantPTrackerId } from './utils/pmtct-helpers'; import { configSchema } from './config-schema'; +import rootComponent from './root.component'; +import { createDashboardLink } from '@openmrs/esm-patient-common-lib'; export const importTranslation = require.context('../translations', false, /.json$/, 'lazy'); @@ -49,16 +50,26 @@ export function startupApp() { name: 'ArtSubmissionAction', load: () => import('./post-submission-actions/art-linkage-action'), }); - registerExpressionHelper('customGenerateInfantPTrackerId', generateInfantPTrackerId); + + import('./utils/pmtct-helpers').then(({ generateInfantPTrackerId }) => { + registerExpressionHelper('customGenerateInfantPTrackerId', generateInfantPTrackerId); + }); } +export const root = getSyncLifecycle(rootComponent, options); + +export const maternalChildDashboardLink = getSyncLifecycle( + createNewOHRIDashboardLink(motherChildDashboardMeta), + options, +); + export const mchDashboard = getSyncLifecycle(createConditionalDashboardGroup(mchFolderMeta), options); export const mchSummaryDashboardLink = getSyncLifecycle( createDashboardLink({ ...mchSummaryDashboardMeta, moduleName }), options, ); -export const mchSummaryDashboard = getSyncLifecycle(MaternalSummary, { +export const mchSummaryDashboard = getAsyncLifecycle(() => import('./views/mch-summary/mch-summary.component'), { featureName: 'mch-summary', moduleName, }); @@ -82,7 +93,6 @@ export const childVisitsDashboard = getSyncLifecycle(ChildHealthList, { featureName: 'maternal-visits', moduleName, }); -export const maternalChildDashboardLink = getSyncLifecycle(createOHRIDashboardLink(motherChildDashboardMeta), options); export const maternalChildDashboard = getSyncLifecycle(OHRIHome, { featureName: 'mother child health results dashboard', moduleName, diff --git a/packages/esm-ohri-pmtct-app/src/post-submission-actions/art-linkage-action.ts b/packages/esm-ohri-pmtct-app/src/post-submission-actions/art-linkage-action.ts index 6e20c5a72..fa2062bfb 100644 --- a/packages/esm-ohri-pmtct-app/src/post-submission-actions/art-linkage-action.ts +++ b/packages/esm-ohri-pmtct-app/src/post-submission-actions/art-linkage-action.ts @@ -13,9 +13,8 @@ const ArtSubmissionAction: PostSubmissionAction = { return; } - let artNumber = encounter.obs.find( - (observation) => observation.concept.uuid === config.obsConcepts.artNoConcept, - )?.value; + let artNumber = encounter.obs.find((observation) => observation.concept.uuid === config.obsConcepts.artNoConcept) + ?.value; if (!artNumber) { return; } @@ -26,7 +25,7 @@ const ArtSubmissionAction: PostSubmissionAction = { //Patient can only have one ART No. const patientIdentifiers = await fetchPatientIdentifiers(patient.id); const existingArtNumbers = patientIdentifiers.filter( - (id) => id.identifierType.uuid === config.encounterTypes.artUniqueNumberType, + (id) => id.identifierType.uuid === config.identifiersTypes.artUniqueNumberType, ); if (existingArtNumbers.length > 0) { return; @@ -35,7 +34,7 @@ const ArtSubmissionAction: PostSubmissionAction = { //add current art number to identities const currentArtNumberObject: PatientIdentifier = { identifier: artNumber, - identifierType: config.encounterTypes.artUniqueNumberType, + identifierType: config.identifiersTypes.artUniqueNumberType, location: encounterLocation, preferred: false, }; diff --git a/packages/esm-ohri-pmtct-app/src/post-submission-actions/current-ptracker-action.ts b/packages/esm-ohri-pmtct-app/src/post-submission-actions/current-ptracker-action.ts index de7a54fd0..214d4ecde 100644 --- a/packages/esm-ohri-pmtct-app/src/post-submission-actions/current-ptracker-action.ts +++ b/packages/esm-ohri-pmtct-app/src/post-submission-actions/current-ptracker-action.ts @@ -18,12 +18,16 @@ export const PTrackerSubmissionAction: PostSubmissionAction = { export async function updatePatientPtracker(encounter, encounterLocation, patientUuid) { const config = await getConfig('@ohri/openmrs-esm-ohri-pmtct'); - const inComingPTrackerID = encounter.obs.find((observation) => observation.concept.uuid === config.obsConcepts.pTrackerIdConcept)?.value; + const inComingPTrackerID = encounter.obs.find( + (observation) => observation.concept.uuid === config.obsConcepts.pTrackerIdConcept, + )?.value; if (!inComingPTrackerID) { return; } const patientIdentifiers = await fetchPatientIdentifiers(patientUuid); - const existingPTrackers = patientIdentifiers.filter((id) => id.identifierType.uuid === config.encounterTypes.PTrackerIdentifierType); + const existingPTrackers = patientIdentifiers.filter( + (id) => id.identifierType.uuid === config.identifiersTypes.ptrackerIdentifierType, + ); if (existingPTrackers.some((ptracker) => ptracker.identifier === inComingPTrackerID)) { return; } @@ -31,7 +35,7 @@ export async function updatePatientPtracker(encounter, encounterLocation, patien //add current ptracker to identities const currentPTrackerObject: PatientIdentifier = { identifier: inComingPTrackerID, - identifierType: config.encounterTypes.PTrackerIdentifierType, + identifierType: config.identifiersTypes.ptrackerIdentifierType, location: encounterLocation, preferred: false, }; diff --git a/packages/esm-ohri-pmtct-app/src/root.component.tsx b/packages/esm-ohri-pmtct-app/src/root.component.tsx new file mode 100644 index 000000000..5136ae080 --- /dev/null +++ b/packages/esm-ohri-pmtct-app/src/root.component.tsx @@ -0,0 +1,27 @@ +import React from 'react'; +import { SWRConfig } from 'swr'; +import { BrowserRouter, Route, Routes } from 'react-router-dom'; +import Home from './home.component'; + +const swrConfiguration = { + // Maximum number of retries when the backend returns an error + errorRetryCount: 3, +}; + +const RootComponent: React.FC = () => { + const mchBasename = window.getOpenmrsSpaBase() + 'home/mother-child-health'; + + return ( +
+ + + + } /> + + + +
+ ); +}; + +export default RootComponent; diff --git a/packages/esm-ohri-pmtct-app/src/routes.json b/packages/esm-ohri-pmtct-app/src/routes.json index 6597cc222..a5021cac3 100644 --- a/packages/esm-ohri-pmtct-app/src/routes.json +++ b/packages/esm-ohri-pmtct-app/src/routes.json @@ -3,9 +3,24 @@ "backendDependencies": { "webservices.rest": "^2.24.0" }, - "pages": [ - ], + "pages": [], "extensions": [ + { + "name": "maternal-child-health-results-summary", + "slot": "homepage-dashboard-slot", + "component": "maternalChildDashboardLink", + "meta": { + "title": "Maternal & Child Health", + "name": "mother-child-health", + "slot": "mother-child-health-dashboard-slot" + }, + "order": 6 + }, + { + "name": "mother-child-health-dashboard", + "slot": "mother-child-health-dashboard-slot", + "component": "root" + }, { "name": "mch", "slot": "patient-chart-dashboard-slot", @@ -62,53 +77,6 @@ "name": "child-visits-summary-ext", "slot": "child-visits-summary-slot", "component": "childVisitsDashboard" - }, - { - "name": "maternal-child-health-results-summary", - "slot": "dashboard-slot", - "component": "maternalChildDashboardLink", - "meta": { - "title": "Maternal & Child Health", - "path": "Maternal & Child Health", - "slot": "mother-child-health-dashboard-slot", - "config": { - "columns": 1, - "type": "grid", - "programme": "pmtct", - "dashboardTitle": "Mother and Child Health" - }, - "isLink": true - } - }, - { - "name": "mother-child-health-results-dashboard", - "slot": "mother-child-health-dashboard-slot", - "component": "maternalChildDashboard", - "meta": { - "name": "mother-child-health", - "slot": "mother-child-health-dashboard-slot", - "config": { - "columns": 1, - "programme": "pmtct", - "dashboardTitle": "Mother Child Health Home Page" - }, - "title": "Maternal & Child Health" - } - }, - { - "name": "pmtct-home-header-slot", - "slot": "pmtct-home-header-slot", - "component": "pmtctDashboardHeader" - }, - { - "name": "pmtct-home-tiles-ext", - "slot": "pmtct-home-tiles-slot", - "component": "pmtctDashboardTiles" - }, - { - "name": "pmtct-home-tabs-ext", - "slot": "pmtct-home-tabs-slot", - "component": "pmtctDashboardTabs" } ] } diff --git a/packages/esm-ohri-pmtct-app/src/views/child-health/tabs/infant-postnatal-care.component.tsx b/packages/esm-ohri-pmtct-app/src/views/child-health/tabs/infant-postnatal-care.component.tsx index 4c4f7b9a4..922cc67fd 100644 --- a/packages/esm-ohri-pmtct-app/src/views/child-health/tabs/infant-postnatal-care.component.tsx +++ b/packages/esm-ohri-pmtct-app/src/views/child-health/tabs/infant-postnatal-care.component.tsx @@ -17,7 +17,7 @@ const InfantPostnatalList: React.FC = ({ patientUuid } const { t } = useTranslation(); const config = useConfig(); const headerTitle = t('hivExposedInfant', 'HIV Exposed Infant'); - const InfantPNCEncounterTypeUUID = config.encounterTypes.infantPostnatal; + const { encounterTypes, formNames, formUuids } = useConfig(); async function fetchMotherName() { let motherName = '--'; @@ -105,8 +105,8 @@ const InfantPostnatalList: React.FC = ({ patientUuid } return ( = ({ patientUuid }) => const { t } = useTranslation(); const config = useConfig(); const headerTitle = t('antenatalCare', 'Antenatal Care'); - const ANCEncounterTypeUUID = config.encounterTypes.antenatal; + const { encounterTypes, formNames, formUuids } = useConfig(); const columns: EncounterListColumn[] = useMemo( () => [ @@ -99,8 +99,8 @@ const AntenatalCareList: React.FC = ({ patientUuid }) => return ( = ({ patientUuid }) const { t } = useTranslation(); const config = useConfig(); const headerTitle = t('labourAndDelivery', 'Labour and Delivery'); - const LNDEncounterTypeUUID = config.encounterTypes.laborAndDelivery; + const { encounterTypes, formNames, formUuids } = useConfig(); const columns: EncounterListColumn[] = useMemo( () => [ @@ -38,10 +38,10 @@ const LabourDeliveryList: React.FC = ({ patientUuid }) }, }, { - key: 'hivTestResults', - header: t('hivTestResults', 'HIV Test Results'), + key: 'hivTestStatus', + header: t('hivTestStatus', 'HIV Test Status'), getValue: (encounter) => { - return getObsFromEncounter(encounter, config.obsConcepts.ancHivResultConcept); + return getObsFromEncounter(encounter, config.obsConcepts.hivTestStatus); }, }, { @@ -85,8 +85,8 @@ const LabourDeliveryList: React.FC = ({ patientUuid }) return ( = ({ patientUuid }) => const { t } = useTranslation(); const config = useConfig(); const headerTitle = t('postnatalCare', 'Postnatal Care'); - const MotherPNCEncounterTypeUUID = config.encounterTypes.motherPostnatal; + const { encounterTypes, formNames, formUuids } = useConfig(); const columns: EncounterListColumn[] = useMemo( () => [ @@ -34,7 +34,7 @@ const PostnatalCareList: React.FC = ({ patientUuid }) => key: 'currentHivStatus', header: t('currentHivStatus', 'Current HIV Status'), getValue: (encounter) => { - return getObsFromEncounter(encounter, config.obsConcepts.MotherHivStatus); + return getObsFromEncounter(encounter, config.obsConcepts.hivTestStatus); }, }, { @@ -85,8 +85,8 @@ const PostnatalCareList: React.FC = ({ patientUuid }) => return ( = ({ patientUuid }) => { const [relativeToIdentifierMap, setRelativeToIdentifierMap] = useState([]); const [pregnancyOutcomes, setPregnancyOutcomes] = useState([]); const [infantOutcomes, setInfantOutcomes] = useState([]); - const { formNames, encounterTypes, obsConcepts } = useConfig(); + const { formNames, encounterTypes, obsConcepts, formUuids } = useConfig(); + const [totalAncCount, setTotalAncCount] = useState(null); + + useEffect(() => { + const fetchData = async () => { + try { + const [totalAncCount] = await Promise.all([fetchMambaReportData('no_of_anc_visits')]); + + setTotalAncCount(totalAncCount); + } catch (error) { + console.error('Error fetching data:', error); + throw new Error('Error fetching data. Please try again.'); + } + }; + + fetchData(); + }, []); const headersFamily = [ { @@ -220,12 +237,32 @@ const CurrentPregnancy: React.FC = ({ patientUuid }) => { { key: 'motherHIVStatus', header: t('motherHIVStatus', 'Mother HIV Status'), - encounterTypes: [encounterTypes.labourAndDelivery], - getObsValue: async ([encounter]) => { - const currentPTrackerId = getObsFromEncounter(encounter, obsConcepts.pTrackerIdConcept); - return '--'; + encounterTypes: [encounterTypes.motherPostnatal, encounterTypes.labourAndDelivery, encounterTypes.antenatal], + getObsValue: (encounters) => { + const pncArtData = { + artInitiation: getObsFromEncounter(encounters[0], obsConcepts.artInitiationConcept), + motherHIVStatus: getObsFromEncounter(encounters[0], obsConcepts.hivTestResultConcept), + pTrackerId: getObsFromEncounter(encounters[0], obsConcepts.pTrackerIdConcept), + }; + const lndArtData = { + artInitiation: getObsFromEncounter(encounters[1], obsConcepts.artInitiationConcept), + motherHIVStatus: getObsFromEncounter(encounters[1], obsConcepts.hivTestResultConcept), + pTrackerId: getObsFromEncounter(encounters[1], obsConcepts.pTrackerIdConcept), + }; + const ancArtData = { + artInitiation: getObsFromEncounter(encounters[2], obsConcepts.artInitiationConcept), + motherHIVStatus: getObsFromEncounter(encounters[2], obsConcepts.hivTestResultConcept), + pTrackerId: getObsFromEncounter(encounters[2], obsConcepts.pTrackerIdConcept), + }; + const latestArtData = getLatestArtDetails(pncArtData, lndArtData, ancArtData); + if (!latestArtData['motherHIVStatus']) { + return '--'; + } + + return latestArtData['motherHIVStatus']; }, }, + { key: 'expectedDeliveryDate', header: t('expectedDeliveryDate', 'Expected Delivery Date'), @@ -246,7 +283,7 @@ const CurrentPregnancy: React.FC = ({ patientUuid }) => { key: 'motherStatus', header: t('motherStatus', 'Mother Status'), encounterTypes: [encounterTypes.labourAndDelivery], - getObsValue: (encounter) => { + getObsValue: async ([encounter]) => { return getObsFromEncounter(encounter, obsConcepts.motherStatusConcept); }, }, @@ -331,14 +368,11 @@ const CurrentPregnancy: React.FC = ({ patientUuid }) => { header: t('ancVisitsAttended', 'ANC visits attended'), encounterTypes: [encounterTypes.antenatal], getObsValue: async ([encounter]) => { - const currentPTrackerId = getObsFromEncounter(encounter, obsConcepts.pTrackerIdConcept); - // const totalVisits = await getAncVisitCount(currentPTrackerId, patientUuid); - // return totalVisits.rows.length ? totalVisits.rows[0].total : '0'; - return '--'; + return totalAncCount; }, }, ], - [], + [totalAncCount], ); const columnsMotherPreviousVisit: EncounterListColumn[] = useMemo( @@ -466,9 +500,9 @@ const CurrentPregnancy: React.FC = ({ patientUuid }) => { description={previousVisitsTitle} headerTitle={previousVisitsTitle} formList={[ - { name: formNames.antenatal }, - { name: formNames.labourAndDelivery }, - { name: formNames.motherPostnatal }, + { name: formNames.antenatal, uuid: '' }, + { name: formNames.labourAndDelivery, uuid: formUuids.labourAndDelivery }, + { name: formNames.motherPostnatal, uuid: formUuids.motherPostnatal }, ]} launchOptions={{ hideFormLauncher: true, diff --git a/packages/esm-ohri-pmtct-app/src/views/mch-summary/tabs/hiv-exposed-infant.component.tsx b/packages/esm-ohri-pmtct-app/src/views/mch-summary/tabs/hiv-exposed-infant.component.tsx index 369f9f550..5d10e26ff 100644 --- a/packages/esm-ohri-pmtct-app/src/views/mch-summary/tabs/hiv-exposed-infant.component.tsx +++ b/packages/esm-ohri-pmtct-app/src/views/mch-summary/tabs/hiv-exposed-infant.component.tsx @@ -22,9 +22,9 @@ const HivExposedInfant: React.FC<{ dateOfBirth: string; }> = ({ patientUuid, dateOfBirth }) => { const { t } = useTranslation(); - const config = useConfig(); const [relatives, setRelatives] = useState([]); const [relativeToIdentifierMap, setRelativeToIdentifierMap] = useState([]); + const { formNames, formUuids, encounterTypes, obsConcepts } = useConfig(); useEffect(() => { getParentRelationships(); @@ -35,33 +35,33 @@ const HivExposedInfant: React.FC<{ { key: 'artProphylaxisStatus', header: t('artProphylaxisStatus', 'ART Prophylaxis Status'), - encounterTypes: [config.encounterTypes.infantPostnatal], + encounterTypes: [encounterTypes.infantPostnatal], getObsValue: ([encounter]) => { - return getObsFromEncounter(encounter, config.obsConcepts.artProphylaxisStatus); + return getObsFromEncounter(encounter, obsConcepts.artProphylaxisStatus); }, }, { key: 'breastfeeding', header: t('breastfeeding', 'Breastfeeding'), - encounterTypes: [config.encounterTypes.infantPostnatal], + encounterTypes: [encounterTypes.infantPostnatal], getObsValue: ([encounter]) => { - return getObsFromEncounter(encounter, config.obsConcepts.breastfeedingStatus); + return getObsFromEncounter(encounter, obsConcepts.breastfeedingStatus); }, }, { key: 'hivStatus', header: t('hivStatus', 'HIV Status'), - encounterTypes: [config.encounterTypes.infantPostnatal], + encounterTypes: [encounterTypes.infantPostnatal], getObsValue: ([encounter]) => { - return getObsFromEncounter(encounter, config.obsConcepts.finalTestResults); + return getObsFromEncounter(encounter, obsConcepts.finalTestResults); }, }, { key: 'finalOutcome', header: t('finalOutcome', 'Final Outcome'), - encounterTypes: [config.encounterTypes.infantPostnatal], + encounterTypes: [encounterTypes.infantPostnatal], getObsValue: ([encounter]) => { - return getObsFromEncounter(encounter, config.obsConcepts.outcomeStatus); + return getObsFromEncounter(encounter, obsConcepts.outcomeStatus); }, }, ], @@ -74,21 +74,21 @@ const HivExposedInfant: React.FC<{ key: 'date', header: t('date', 'Date'), getValue: (encounter) => { - return getObsFromEncounter(encounter, config.obsConcepts.artStartDate, true); + return getObsFromEncounter(encounter, obsConcepts.artStartDate, true); }, }, { key: 'testType', header: t('testType', 'Test Type'), getValue: (encounter) => { - return getObsFromEncounter(encounter, config.obsConcepts.testTypeConcept); + return getObsFromEncounter(encounter, obsConcepts.testTypeConcept); }, }, { key: 'ageAtTimeOfTest', header: t('ageAtTimeOfTest', 'Age at time of test'), getValue: (encounter) => { - const artDate = getObsFromEncounter(encounter, config.obsConcepts.artStartDate); + const artDate = getObsFromEncounter(encounter, obsConcepts.artStartDate); return artDate ? dayjs().diff(dayjs(artDate), 'day') : '--'; }, }, @@ -96,7 +96,7 @@ const HivExposedInfant: React.FC<{ key: 'hivStatus', header: t('hivStatus', 'HIV Status'), getValue: (encounter) => { - return getObsFromEncounter(encounter, config.obsConcepts.finalTestResults); + return getObsFromEncounter(encounter, obsConcepts.finalTestResults); }, }, ]; @@ -148,7 +148,7 @@ const HivExposedInfant: React.FC<{ const identifiers = await fetchPatientIdentifiers(patientUuid); if (identifiers) { pTrackerMap.pTrackerId = identifiers.find( - (id) => id.identifierType.uuid === config.encounterTypes.PTrackerIdentifierType, + (id) => id.identifierType.uuid === encounterTypes.PTrackerIdentifierType, ).identifier; pTrackerMap.patientId = patientUuid; } @@ -193,7 +193,7 @@ const HivExposedInfant: React.FC<{ key: 'visitDate', header: t('visitDate', 'Visit date'), getValue: (encounter) => { - return getObsFromEncounter(encounter, config.obsConcepts.infantVisitDate, true); + return getObsFromEncounter(encounter, obsConcepts.infantVisitDate, true); }, }, { @@ -207,7 +207,7 @@ const HivExposedInfant: React.FC<{ key: 'nextFollowUpDate', header: t('nextFollowUpDate', 'Next Follow-up date'), getValue: (encounter) => { - return getObsFromEncounter(encounter, config.obsConcepts.followUpDateConcept, true); + return getObsFromEncounter(encounter, obsConcepts.followUpDateConcept, true); }, }, { @@ -239,8 +239,8 @@ const HivExposedInfant: React.FC<{ = ({ patientUuid }) => { +const LabResultsSummary: React.FC = () => { const { t } = useTranslation(); return ( diff --git a/packages/esm-opd-app/package.json b/packages/esm-opd-app/package.json new file mode 100644 index 000000000..e19125bda --- /dev/null +++ b/packages/esm-opd-app/package.json @@ -0,0 +1,55 @@ +{ + "name": "@ohri/openmrs-esm-ohri-opd-app", + "version": "2.3.3", + "description": "opd active visits", + "browser": "dist/openmrs-esm-ohri-opd-app.js", + "main": "src/index.ts", + "license": "MIT", + "homepage": "https://github.com/UCSF-IGHS/openmrs-esm-ohri#readme", + "scripts": { + "start": "openmrs develop", + "serve": "webpack serve --mode=development", + "debug": "npm run serve", + "build": "webpack --mode production", + "analyze": "webpack --mode=production --env.analyze=true", + "lint": "eslint src --ext tsx", + "typescript": "tsc", + "prepublishOnly": "npm run build", + "extract-translations": "i18next 'src/**/*.component.tsx'" + }, + "browserslist": [ + "extends browserslist-config-openmrs" + ], + "keywords": [ + "openmrs", + "ohri", + "opd" + ], + "publishConfig": { + "access": "public" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/UCSF-IGHS/openmrs-esm-ohri.git" + }, + "bugs": { + "url": "https://github.com/UCSF-IGHS/openmrs-esm-ohri/issues" + }, + "dependencies": { + "@carbon/react": "^1.13.0", + "@ohri/esm-patient-chart-app": "7.1.0-5" + }, + "peerDependencies": { + "@ohri/esm-patient-chart-app": "7.1.0-5", + "@openmrs/esm-framework": "5.x", + "@openmrs/esm-patient-common-lib": "6.x", + "dayjs": "1.x", + "react": "18.x", + "react-i18next": "11.x", + "swr": "2.x" + }, + "devDependencies": { + "@ohri/esm-patient-chart-app": "7.1.0-5", + "webpack": "^5.88.2" + } +} diff --git a/packages/esm-opd-app/src/config-schema.ts b/packages/esm-opd-app/src/config-schema.ts new file mode 100644 index 000000000..11f523bd0 --- /dev/null +++ b/packages/esm-opd-app/src/config-schema.ts @@ -0,0 +1 @@ +export const configSchema = {}; diff --git a/packages/esm-opd-app/src/dashboard.meta.tsx b/packages/esm-opd-app/src/dashboard.meta.tsx new file mode 100644 index 000000000..29c682181 --- /dev/null +++ b/packages/esm-opd-app/src/dashboard.meta.tsx @@ -0,0 +1,12 @@ +export const opdFolderMeta = { + title: 'OPD', + slotName: 'opd-slot', + isExpanded: false, +}; + +export const activeVisitDashboardMeta = { + slot: 'patient-chart-active-visit-dashboard-slot', + columns: 1, + path: 'Active Visits', + title: 'Active Visits', +}; diff --git a/packages/esm-opd-app/src/dashboard.scss b/packages/esm-opd-app/src/dashboard.scss new file mode 100644 index 000000000..009007b57 --- /dev/null +++ b/packages/esm-opd-app/src/dashboard.scss @@ -0,0 +1,21 @@ +@import "./root.scss"; + +.noMarker { + list-style-type: none; +} + +.noMarker ul li a { + padding-left: 40px !important; + font: lighter; +} + +.currentNavItem > a { + background-color: #cecece !important; + color: #161616 !important; + border-left-color: var(--brand-01) !important; + font: bolder; +} + +.hide { + display: none; +} diff --git a/packages/esm-opd-app/src/declarations.d.tsx b/packages/esm-opd-app/src/declarations.d.tsx new file mode 100644 index 000000000..875203d56 --- /dev/null +++ b/packages/esm-opd-app/src/declarations.d.tsx @@ -0,0 +1,2 @@ +declare module '*.css'; +declare module '*.scss'; diff --git a/packages/esm-opd-app/src/index.ts b/packages/esm-opd-app/src/index.ts new file mode 100644 index 000000000..14733e5e2 --- /dev/null +++ b/packages/esm-opd-app/src/index.ts @@ -0,0 +1,29 @@ +import { defineConfigSchema, getAsyncLifecycle, getSyncLifecycle, registerFeatureFlag } from '@openmrs/esm-framework'; +import { opdFolderMeta, activeVisitDashboardMeta } from './dashboard.meta'; +import { createDashboardGroup, createDashboardLink } from '@openmrs/esm-patient-common-lib'; +import { configSchema } from './config-schema'; + +export const importTranslation = require.context('../translations', false, /.json$/, 'lazy'); + +require('./root.scss'); + +export const moduleName = '@ohri/openmrs-esm-ohri-opd-app'; + +const options = { + featureName: 'ohri-opd', + moduleName, +}; + +export function startupApp() { + defineConfigSchema(moduleName, configSchema); +} + +export const opdPatientChartDashboard = getSyncLifecycle(createDashboardGroup(opdFolderMeta), options); + +export const activeVisitDashboardLink = getSyncLifecycle( + createDashboardLink({ + ...activeVisitDashboardMeta, + moduleName, + }), + { featureName: 'active-visit', moduleName }, +); diff --git a/packages/esm-opd-app/src/root.scss b/packages/esm-opd-app/src/root.scss new file mode 100644 index 000000000..c504fa3d9 --- /dev/null +++ b/packages/esm-opd-app/src/root.scss @@ -0,0 +1,116 @@ +@use '@carbon/styles/scss/spacing'; +@use '@carbon/styles/scss/type'; +@import '~@openmrs/esm-styleguide/src/vars'; + +$ui-01: #f4f4f4; +$ui-02: #ffffff; +$ui-03: #e0e0e0; +$ui-05: #161616; +$ui-background: #ffffff; +$color-gray-70: #525252; +$color-blue-60-2: #0f62fe; +$color-yellow-50: #feecae; +$inverse-support-03: #f1c21b; +$warning-background: #fff8e1; +$openmrs-background-grey: #f4f4f4; +$danger: #da1e28; +$interactive-01: #0f62fe; +$brand-teal-01: #3197D9; +$ohri-input-width: 22.313rem; +$ohri-home-background: #ededed; +$button-primary: #0078A6; + +.productiveHeading01 { + @include type.type-style("heading-compact-01"); +} + +.productiveHeading02 { + @include type.type-style("heading-compact-02"); +} + +.productiveHeading03 { + @include type.type-style("heading-03"); +} + +.productiveHeading04 { + @include type.type-style("heading-04"); +} + +.productiveHeading05 { + @include type.type-style("heading-05"); +} + +.productiveHeading06 { + @include type.type-style("heading-06"); +} + +.bodyShort01 { + @include type.type-style("body-compact-01"); +} + +.helperText01 { + @include type.type-style("helper-text-01"); +} + +.bodyShort02 { + @include type.type-style("body-compact-02"); +} + +.bodyLong01 { + @include type.type-style("body-01"); +} + +.bodyLong02 { + @include type.type-style("body-02"); +} + +.label01 { + @include type.type-style("label-01"); +} + +.text02 { + color: $text-02; +} + +aside { + background-color: $ui-02 !important; +} + +// Login Overrides + +div[class*='-esm-login__styles__center'] > img { + width: 140px; // design has 120px +} + +:global(.tab-12rem) > button { + width: 12rem !important; +} + +:global(.tab-14rem) > button { + width: 14rem !important; +} + +:global(.tab-16rem) > button { + width: 16rem !important; +} + +:global(.cds--overflow-menu) > div { + width: 15rem !important; +} + +nav :global(.cds--accordion__title) { + color: #525252; + font-weight: 600 !important; +} + +nav :global(.cds--accordion__content) { + padding-bottom: 0 !important; + padding-top: 0 !important; +} + +nav :global(.cds--accordion__content) > a { + background-color: #cecece !important; + color: #161616 !important; + border-left-color: var(--brand-01) !important; + font: bolder; +} diff --git a/packages/esm-opd-app/src/routes.json b/packages/esm-opd-app/src/routes.json new file mode 100644 index 000000000..a2c331b41 --- /dev/null +++ b/packages/esm-opd-app/src/routes.json @@ -0,0 +1,38 @@ +{ + "$schema": "https://json.openmrs.org/routes.schema.json", + "backendDependencies": { + "webservices.rest": "^2.24.0" + }, + "pages": [ + ], + "extensions": [ + { + "name": "opd", + "slot": "patient-chart-dashboard-slot", + "component": "opdPatientChartDashboard", + "order": 25, + "meta": { + "path": "OPD", + "slot": "opd-slot", + "isExpanded": false + } + }, + { + "name": "active-visit-summary-dashboard", + "component": "activeVisitDashboardLink", + "slot": "opd-slot", + "meta": { + "slot": "patient-chart-active-visit-dashboard-slot", + "columns": 1, + "path": "Active Visits" + } + }, + { + "name": "opd-summary-ext", + "slot": "opd-summary-slot", + "component": "opdSummaryDashboard" + } + + + ] +} diff --git a/packages/esm-opd-app/src/setupTests.ts b/packages/esm-opd-app/src/setupTests.ts new file mode 100644 index 000000000..3abed9870 --- /dev/null +++ b/packages/esm-opd-app/src/setupTests.ts @@ -0,0 +1,15 @@ +import '@testing-library/jest-dom'; + +declare global { + interface Window { + openmrsBase: string; + spaBase: string; + } +} + +const { getComputedStyle } = window; +window.getComputedStyle = (element) => getComputedStyle(element); +window.openmrsBase = '/openmrs'; +window.spaBase = '/spa'; +window.getOpenmrsSpaBase = () => '/openmrs/spa/'; +window.HTMLElement.prototype.scrollIntoView = jest.fn(); diff --git a/packages/esm-opd-app/translations/en.json b/packages/esm-opd-app/translations/en.json new file mode 100644 index 000000000..0967ef424 --- /dev/null +++ b/packages/esm-opd-app/translations/en.json @@ -0,0 +1 @@ +{} diff --git a/packages/esm-opd-app/tsconfig.json b/packages/esm-opd-app/tsconfig.json new file mode 100644 index 000000000..db240ff83 --- /dev/null +++ b/packages/esm-opd-app/tsconfig.json @@ -0,0 +1,28 @@ +{ + "compilerOptions": { + "esModuleInterop": true, + "module": "esnext", + "allowSyntheticDefaultImports": true, + "jsx": "react", + "skipLibCheck": true, + "moduleResolution": "node", + "lib": [ + "dom", + "es5", + "scripthost", + "es2015", + "es2015.promise", + "es2016.array.include", + "es2018", + "es2020" + ], + "resolveJsonModule": true, + "noEmit": true, + "target": "esnext", + "paths": { + "@openmrs/*": [ + "./node_modules/@openmrs/*" + ] + } + } +} diff --git a/packages/esm-opd-app/webpack.config.js b/packages/esm-opd-app/webpack.config.js new file mode 100644 index 000000000..b15408968 --- /dev/null +++ b/packages/esm-opd-app/webpack.config.js @@ -0,0 +1,17 @@ +const path = require('path'); +const config = (module.exports = require('openmrs/default-webpack-config')); +config.scriptRuleConfig.exclude = + path.sep == '/' + ? /(node_modules[^\/@openmrs\/esm\-patient\-common\-lib])/ + : /(node_modules[^\\@openmrs\/esm\-patient\-common\-lib])/; + +// Temporary fix to resolve webpack issues with imports from the commons library +config.overrides.resolve = { + extensions: ['.tsx', '.ts', '.jsx', '.js', '.scss'], + alias: { + '@openmrs/esm-framework': '@openmrs/esm-framework/src/internal', + '@ohri/openmrs-esm-ohri-commons-lib': path.resolve(__dirname, '../esm-commons-lib/src/index'), + '@openmrs/openmrs-form-engine-lib': '@openmrs/openmrs-form-engine-lib/src/index', + }, +}; +module.exports = config; diff --git a/packages/esm-tb-app/package.json b/packages/esm-tb-app/package.json index 05af92158..4a4d27ba0 100644 --- a/packages/esm-tb-app/package.json +++ b/packages/esm-tb-app/package.json @@ -1,6 +1,6 @@ { "name": "@ohri/openmrs-esm-ohri-tb-app", - "version": "2.2.0", + "version": "2.3.3", "description": "Tuberclosis microfrontend for OpenMRS HIV Reference Implementation (OHRI)", "browser": "dist/openmrs-esm-ohri-tb-app.js", "main": "src/index.ts", diff --git a/packages/esm-tb-app/src/config-schema.ts b/packages/esm-tb-app/src/config-schema.ts index c4771d9d3..b0c9fc217 100644 --- a/packages/esm-tb-app/src/config-schema.ts +++ b/packages/esm-tb-app/src/config-schema.ts @@ -59,6 +59,7 @@ export const configSchema = { tptOutcome: '1266AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', tptOutcomeDate: '163284AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', tptDateOutcome: '163284AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', + treatmentPlan: '1265AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', }, }, cohorts: { @@ -75,6 +76,22 @@ export const configSchema = { _default: { TptOutcomeFormName: 'TPT outcome form', TptCaseEnrolmentFormName: 'TPT Case Enrolment form', + TptTreatmentFormName: 'TPT Followup & Treatment form', + tbfollowUpForm: 'TB Follow-up Form', + tbPatientTracingForm: 'TB Patient Tracing Form', + tbContactListingForm: 'TB Contact Listing', + }, + }, + formUuids: { + _type: Type.Object, + _description: 'TPT Form Uuids.', + _default: { + tptOutcomeFormUuid: '97fa657f-7627-3f81-829d-826b0d4c7d28', + tptCaseEnrolmentFormUuid: '71080512-07e6-345f-864f-93e892420258', + tptTreatmentFormUuid: '6212bb87-766e-33d0-b37b-79f019008492', + tbFollowUpFormUuid: '79b05c37-0def-4482-a345-c63b99fbd565', + tbPatientTracingFormUuid: 'ac3cbd10-1939-3797-9de6-2a39dfb68441', + tbContactListingFormUuid: 'cb16d920-62f1-3696-b781-e6a4f5e80de1', }, }, }; diff --git a/packages/esm-tb-app/src/dashboard.meta.tsx b/packages/esm-tb-app/src/dashboard.meta.tsx index e9e5fe372..4d3d5ea58 100644 --- a/packages/esm-tb-app/src/dashboard.meta.tsx +++ b/packages/esm-tb-app/src/dashboard.meta.tsx @@ -51,11 +51,29 @@ export const tbPreventionDashboardMeta = { title: 'TB Prevention', }; +export const tbPreventionDashboardMeta = { + name: 'tb-prevention', + slot: 'tb-prevention-dashboard-slot', + title: 'TB Prevention', + isFolder: true, + folderTitle: 'Tuberculosis', + folderIcon: PillsAdd, + isHidden: true, +}; + export const tbCasesDashboardMeta = { name: 'tb-cases', slot: 'tb-cases-dashboard-slot', - config: { columns: 1, type: 'grid', programme: 'tb', dashboardTitle: 'TB Treatment' }, title: 'TB Treatment', + isFolder: true, + childLinks: [ + { + name: 'tb-prevention', + title: 'TB Prevention', + }, + ], + folderTitle: 'Tuberculosis', + folderIcon: PillsAdd, }; export const tptPatientChartMeta = { diff --git a/packages/esm-tb-app/src/home.component.tsx b/packages/esm-tb-app/src/home.component.tsx new file mode 100644 index 000000000..b91b37c7d --- /dev/null +++ b/packages/esm-tb-app/src/home.component.tsx @@ -0,0 +1,16 @@ +import { OHRIWelcomeSection } from '@ohri/openmrs-esm-ohri-commons-lib'; +import React from 'react'; +import TbHomePatientTabs from './views/dashboard/patient-list-tabs/tb-patient-list-tabs.component'; +import TbSummaryTiles from './views/dashboard/summary-tiles/tb-summary-tiles.component'; + +const Homecomponent = () => { + return ( +
+ + + +
+ ); +}; + +export default Homecomponent; diff --git a/packages/esm-tb-app/src/index.ts b/packages/esm-tb-app/src/index.ts index 0845760dd..2ed8f5f6d 100644 --- a/packages/esm-tb-app/src/index.ts +++ b/packages/esm-tb-app/src/index.ts @@ -1,5 +1,10 @@ -import { defineConfigSchema, getSyncLifecycle } from '@openmrs/esm-framework'; -import { createOHRIDashboardLink, OHRIHome, OHRIWelcomeSection } from '@ohri/openmrs-esm-ohri-commons-lib'; +import { defineConfigSchema, getAsyncLifecycle, getSyncLifecycle } from '@openmrs/esm-framework'; +import { + createOHRIDashboardLink, + OHRIHome, + OHRIWelcomeSection, + createOHRIGroupedLink, +} from '@ohri/openmrs-esm-ohri-commons-lib'; import { createDashboardGroup, createDashboardLink } from '@openmrs/esm-patient-common-lib'; import { tbPatientChartMeta, @@ -25,6 +30,8 @@ import tptProgramManagementSummary from './views/tpt/program-management/tpt-prog import tptPatientSummary from './views/tpt/patient-summary/patient-summary.component'; import TptPreventionSummaryTiles from './views/dashboard/summary-tiles/tpt-summary-tiles.component'; import TptPatientListTabs from './views/dashboard/patient-list-tabs/tpt-patient-list-tabs.component'; +import rootComponent from './root.component'; +import TptHomeComponent from './tpt-home.component'; export const importTranslation = require.context('../translations', false, /.json$/, 'lazy'); @@ -129,13 +136,9 @@ export const tbClinicalViewDashboardLink = getSyncLifecycle( createOHRIDashboardLink(tbClinicalViewDashboardMeta), options, ); -export const tbCasesDashboardLink = getSyncLifecycle(createOHRIDashboardLink(tbCasesDashboardMeta), options); -export const tbCasesDashboard = getSyncLifecycle(OHRIHome, { - featureName: 'tb cases dashboard', - moduleName, -}); -export const tbPreventionDashboardLink = getSyncLifecycle(createOHRIDashboardLink(tbPreventionDashboardMeta), options); -export const tbPreventionDashboard = getSyncLifecycle(OHRIHome, { - featureName: 'tpt cases dashboard', - moduleName, -}); + +export const tbCasesDashboardLink = getSyncLifecycle(createOHRIGroupedLink(tbCasesDashboardMeta), options); +export const tbCasesDashboard = getSyncLifecycle(rootComponent, options); + +export const tbPreventionDashboardLink = getSyncLifecycle(createOHRIGroupedLink(tbPreventionDashboardMeta), options); +export const tbPreventionDashboard = getSyncLifecycle(TptHomeComponent, options); diff --git a/packages/esm-tb-app/src/root.component.tsx b/packages/esm-tb-app/src/root.component.tsx new file mode 100644 index 000000000..58c623831 --- /dev/null +++ b/packages/esm-tb-app/src/root.component.tsx @@ -0,0 +1,27 @@ +import React from 'react'; +import { SWRConfig } from 'swr'; +import { BrowserRouter, Route, Routes } from 'react-router-dom'; +import Home from './home.component'; + +const swrConfiguration = { + // Maximum number of retries when the backend returns an error + errorRetryCount: 3, +}; + +const Root: React.FC = () => { + const tbBasename = window.getOpenmrsSpaBase() + 'home/tb-cases'; + + return ( +
+ + + + } /> + + + +
+ ); +}; + +export default Root; diff --git a/packages/esm-tb-app/src/routes.json b/packages/esm-tb-app/src/routes.json index 53154a6a4..aed35ebbb 100644 --- a/packages/esm-tb-app/src/routes.json +++ b/packages/esm-tb-app/src/routes.json @@ -73,6 +73,39 @@ "slot": "tb-program-management-summary-slot", "component": "tbProgramManagementDashboard" }, + { + "name": "tpt-patient-summary", + "slot": "ohri-tpt-slot", + "component": "tptPatientSummaryDashboardLink", + "meta": { + "slot": "tpt-patient-summary-slot", + "columns": 1, + "path": "tpt-patient-summary", + "layoutMode": "anchored" + } + }, + { + "name": "tpt-patient-summary-ext", + "slot": "tpt-patient-summary-slot", + "component": "tptPatientSummaryDashboard" + }, + + { + "name": "tpt-program-management-summary", + "slot": "ohri-tpt-slot", + "component": "tptProgramManagementDashboardLink", + "meta": { + "slot": "tpt-program-management-summary-slot", + "columns": 1, + "path": "tpt-program-management", + "layoutMode": "anchored" + } + }, + { + "name": "tpt-program-management-summary-ext", + "slot": "tpt-program-management-summary-slot", + "component": "tptProgramManagementDashboard" + }, { @@ -152,6 +185,37 @@ "title": "Tuberculosis" } }, + + { + "name": "tb-home-header-ext", + "slot": "tb-home-header-slot", + "component": "tbDashboardHeader" + }, + { + "name": "tb-home-tiles-ext", + "slot": "tb-home-tiles-slot", + "component": "tbDashboardTiles" + }, + { + "name": "tb-home-tabs-ext", + "slot": "tb-home-tabs-slot", + "component": "tbDashboardTabs" + }, + { + "name": "tpt-home-header-ext", + "slot": "tpt-home-header-slot", + "component": "tptDashboardHeader" + }, + { + "name": "tpt-home-tiles-ext", + "slot": "tpt-home-tiles-slot", + "component": "tptDashboardTiles" + }, + { + "name": "tpt-home-tabs-ext", + "slot": "tpt-home-tabs-slot", + "component": "tptDashboardTabs" + }, { "name": "tb-prevention-dashboard-ext", "slot": "tb-clinical-dashboard-slot", @@ -185,64 +249,33 @@ }, { "name": "tb-cases-dashboard-ext", - "slot": "tb-clinical-dashboard-slot", + "slot": "homepage-dashboard-slot", "component": "tbCasesDashboardLink", "meta": { "name": "tb-cases", "slot": "tb-cases-dashboard-slot", - "config": { - "columns": 1, - "type": "grid", - "programme": "tb", - "dashboardTitle": "TB Treatment" - }, "title": "TB Treatment" } }, { "name": "tb-cases-dashboard", "slot": "tb-cases-dashboard-slot", - "component": "tbCasesDashboard", - "meta": { - "name": "tb-cases", - "slot": "tb-cases-dashboard-slot", - "config": { - "columns": 1, - "programme": "tb", - "dashboardTitle": "TB Treatment" - }, - "title": "TB Treatment" - } - }, - { - "name": "tb-home-header-ext", - "slot": "tb-home-header-slot", - "component": "tbDashboardHeader" + "component": "tbCasesDashboard" }, { - "name": "tb-home-tiles-ext", - "slot": "tb-home-tiles-slot", - "component": "tbDashboardTiles" - }, - { - "name": "tb-home-tabs-ext", - "slot": "tb-home-tabs-slot", - "component": "tbDashboardTabs" - }, - { - "name": "tpt-home-header-ext", - "slot": "tpt-home-header-slot", - "component": "tptDashboardHeader" - }, - { - "name": "tpt-home-tiles-ext", - "slot": "tpt-home-tiles-slot", - "component": "tptDashboardTiles" + "name": "tb-prevention-dashboard-ext", + "slot": "homepage-dashboard-slot", + "component": "tbPreventionDashboardLink", + "meta": { + "name": "tb-prevention", + "slot": "tb-prevention-dashboard-slot", + "title": "TB Prevention" + } }, { - "name": "tpt-home-tabs-ext", - "slot": "tpt-home-tabs-slot", - "component": "tptDashboardTabs" + "name": "tb-prevention-dashboard", + "slot": "tb-prevention-dashboard-slot", + "component": "tbPreventionDashboard" } ] -} +} \ No newline at end of file diff --git a/packages/esm-tb-app/src/tpt-home.component.tsx b/packages/esm-tb-app/src/tpt-home.component.tsx new file mode 100644 index 000000000..83e6a5a4a --- /dev/null +++ b/packages/esm-tb-app/src/tpt-home.component.tsx @@ -0,0 +1,16 @@ +import { OHRIWelcomeSection } from '@ohri/openmrs-esm-ohri-commons-lib'; +import React from 'react'; +import TbSummaryTiles from './views/dashboard/summary-tiles/tb-summary-tiles.component'; +import TptPatientListTabs from './views/dashboard/patient-list-tabs/tpt-patient-list-tabs.component'; + +const TptHomeComponent = () => { + return ( +
+ + + +
+ ); +}; + +export default TptHomeComponent; diff --git a/packages/esm-tb-app/src/tpt-root.component.tsx b/packages/esm-tb-app/src/tpt-root.component.tsx new file mode 100644 index 000000000..98379012d --- /dev/null +++ b/packages/esm-tb-app/src/tpt-root.component.tsx @@ -0,0 +1,27 @@ +import React from 'react'; +import { SWRConfig } from 'swr'; +import { BrowserRouter, Route, Routes } from 'react-router-dom'; +import TptHome from './tpt-home.component'; + +const swrConfiguration = { + // Maximum number of retries when the backend returns an error + errorRetryCount: 3, +}; + +const TptRoot: React.FC = () => { + const tbBasename = window.getOpenmrsSpaBase() + 'home/tpt-cases'; + + return ( +
+ + + + } /> + + + +
+ ); +}; + +export default TptRoot; diff --git a/packages/esm-tb-app/src/views/dashboard/summary-tiles/tb-summary-tiles.component.tsx b/packages/esm-tb-app/src/views/dashboard/summary-tiles/tb-summary-tiles.component.tsx index e86b6c99d..e309f4970 100644 --- a/packages/esm-tb-app/src/views/dashboard/summary-tiles/tb-summary-tiles.component.tsx +++ b/packages/esm-tb-app/src/views/dashboard/summary-tiles/tb-summary-tiles.component.tsx @@ -2,7 +2,7 @@ import { OHRIProgrammeSummaryTiles, fetchMambaReportData } from '@ohri/openmrs-e import React, { useEffect, useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; -function TbSummaryTiles({ launchWorkSpace }) { +function TbSummaryTiles() { const { t } = useTranslation(); const [activeDSClientsCount, setActiveDSClientsCount] = useState(null); const [activeDRClientsCount, setActiveDRClientsCount] = useState(null); @@ -39,7 +39,7 @@ function TbSummaryTiles({ launchWorkSpace }) { linkAddress: '#', subTitle: t('drugResistant', 'Cases with drug resistant TB'), value: activeDRClientsCount, - } + }, ], [activeDSClientsCount, activeDRClientsCount], ); diff --git a/packages/esm-tb-app/src/views/dashboard/summary-tiles/tpt-summary-tiles.component.tsx b/packages/esm-tb-app/src/views/dashboard/summary-tiles/tpt-summary-tiles.component.tsx index 06e839afb..2f2c367bb 100644 --- a/packages/esm-tb-app/src/views/dashboard/summary-tiles/tpt-summary-tiles.component.tsx +++ b/packages/esm-tb-app/src/views/dashboard/summary-tiles/tpt-summary-tiles.component.tsx @@ -2,7 +2,7 @@ import { OHRIProgrammeSummaryTiles } from '@ohri/openmrs-esm-ohri-commons-lib'; import React, { useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; -function TptPreventionSummaryTiles({}) { +function TptPreventionSummaryTiles() { const { t } = useTranslation(); const [activeTptClientsCount] = useState(78); const tiles = useMemo( diff --git a/packages/esm-tb-app/src/views/patient-summary/tb-patient-summary.component.tsx b/packages/esm-tb-app/src/views/patient-summary/tb-patient-summary.component.tsx index 14806bfe1..b9f14b966 100644 --- a/packages/esm-tb-app/src/views/patient-summary/tb-patient-summary.component.tsx +++ b/packages/esm-tb-app/src/views/patient-summary/tb-patient-summary.component.tsx @@ -16,77 +16,77 @@ import { getTbRegimen } from '../../tb-helper'; const TBSummaryOverviewList: React.FC = ({ patientUuid }) => { const { t } = useTranslation(); - const config = useConfig(); const headerRecentTB = t('recentTuberculosis', 'Recent Tuberculosis'); const headerPreviousCases = t('previousCases', 'Previous Cases'); const headerVisit = t('visits', 'Visits'); + const { formNames, formUuids, encounterTypes, obsConcepts } = useConfig(); const recentTuberclosisColumns: SummaryCardColumn[] = useMemo( () => [ { key: 'caseID', header: t('caseID', 'Case ID'), - encounterTypes: [config.encounterTypes.tbProgramEnrollment], + encounterTypes: [encounterTypes.tbProgramEnrollment], getObsValue: async ([encounter]) => { - return getObsFromEncounter(encounter, config.obsConcepts.caseID); + return getObsFromEncounter(encounter, obsConcepts.caseID); }, }, { key: 'enrollmentDate', header: t('enrollmentDate', 'Enrollment Date'), - encounterTypes: [config.encounterTypes.tbProgramEnrollment], + encounterTypes: [encounterTypes.tbProgramEnrollment], getObsValue: async ([encounter]) => { - return getObsFromEncounter(encounter, config.obsConcepts.enrollmentDate, true); + return getObsFromEncounter(encounter, obsConcepts.enrollmentDate, true); }, }, { key: 'type', header: t('type', 'Type'), - encounterTypes: [config.encounterTypes.tbProgramEnrollment], + encounterTypes: [encounterTypes.tbProgramEnrollment], getObsValue: ([encounter]) => { - return getObsFromEncounter(encounter, config.obsConcepts.type); + return getObsFromEncounter(encounter, obsConcepts.type); }, }, { key: 'site', header: t('site', 'Site'), - encounterTypes: [config.encounterTypes.tbProgramEnrollment], + encounterTypes: [encounterTypes.tbProgramEnrollment], getObsValue: ([encounter]) => { - return getObsFromEncounter(encounter, config.obsConcepts.site); + return getObsFromEncounter(encounter, obsConcepts.site); }, }, { key: 'drugSensitivity', header: t('drugSensitivity', 'Drug Sensitivity'), - encounterTypes: [config.encounterTypes.tbProgramEnrollment], + encounterTypes: [encounterTypes.tbProgramEnrollment], getObsValue: ([encounter]) => { - return getObsFromEncounter(encounter, config.obsConcepts.drugSensitivity); + return getObsFromEncounter(encounter, obsConcepts.drugSensitivity); }, }, { key: 'regimen', header: t('regimen', 'Regimen'), - encounterTypes: [config.encounterTypes.tbProgramEnrollment], + encounterTypes: [encounterTypes.tbProgramEnrollment], getObsValue: ([encounter]) => { - const tBEnrollmentType = findObs(encounter, config.obsConcepts.tBEnrollmentType)?.value?.uuid; + const tBEnrollmentType = findObs(encounter, obsConcepts.tBEnrollmentType)?.value?.uuid; return getTbRegimen(encounter, tBEnrollmentType); }, }, { key: 'hivStatus', header: t('hivStatus', 'HIV Status'), - encounterTypes: [config.encounterTypes.tbProgramEnrollment], + encounterTypes: [encounterTypes.tbProgramEnrollment], getObsValue: ([encounter]) => { - return getObsFromEncounter(encounter, config.obsConcepts.hivStatus); + return getObsFromEncounter(encounter, obsConcepts.hivStatus); }, }, { key: 'outcome', header: t('outcome', 'Outcome'), - encounterTypes: [config.encounterTypes.tbProgramEnrollment], + encounterTypes: [encounterTypes.tbProgramEnrollment], getObsValue: ([encounter]) => { - return getObsFromEncounter(encounter, config.obsConcepts.outcome); + return getObsFromEncounter(encounter, obsConcepts.outcome); }, }, ], @@ -99,35 +99,35 @@ const TBSummaryOverviewList: React.FC = ({ patientUuid }) => key: 'caseID', header: t('caseID', 'Case ID'), getValue: (encounter) => { - return getObsFromEncounter(encounter, config.obsConcepts.caseID); + return getObsFromEncounter(encounter, obsConcepts.caseID); }, }, { key: 'enrollmentDate', header: t('enrollmentDate', 'Enrollment Date'), getValue: (encounter) => { - return getObsFromEncounter(encounter, config.obsConcepts.enrollmentDate, true); + return getObsFromEncounter(encounter, obsConcepts.enrollmentDate, true); }, }, { key: 'type', header: t('type', 'Type'), getValue: (encounter) => { - return getObsFromEncounter(encounter, config.obsConcepts.type); + return getObsFromEncounter(encounter, obsConcepts.type); }, }, { key: 'site', header: t('site', 'Site'), getValue: (encounter) => { - return getObsFromEncounter(encounter, config.obsConcepts.site); + return getObsFromEncounter(encounter, obsConcepts.site); }, }, { key: 'regimen', header: t('regimen', 'Regimen'), getValue: (encounter) => { - const tBEnrollmentType = findObs(encounter, config.obsConcepts.tBEnrollmentType)?.value?.uuid; + const tBEnrollmentType = findObs(encounter, obsConcepts.tBEnrollmentType)?.value?.uuid; return getTbRegimen(encounter, tBEnrollmentType); }, }, @@ -135,7 +135,7 @@ const TBSummaryOverviewList: React.FC = ({ patientUuid }) => key: 'outcome', header: t('outcome', 'Outcome'), getValue: (encounter) => { - return getObsFromEncounter(encounter, config.obsConcepts.outcome); + return getObsFromEncounter(encounter, obsConcepts.outcome); }, }, ], @@ -148,42 +148,42 @@ const TBSummaryOverviewList: React.FC = ({ patientUuid }) => key: 'caseID', header: t('caseID', 'Case ID'), getValue: (encounter) => { - return getObsFromEncounter(encounter, config.obsConcepts.caseID); + return getObsFromEncounter(encounter, obsConcepts.caseID); }, }, { key: 'dateOfVisit', header: t('dateOfVisit', 'Date of Visit'), getValue: (encounter) => { - return getObsFromEncounter(encounter, config.obsConcepts.visitDate, true); + return getObsFromEncounter(encounter, obsConcepts.visitDate, true); }, }, { key: 'monthOfVisit', header: t('monthOfVisit', 'Month of Visit'), getValue: (encounter) => { - return getObsFromEncounter(encounter, config.obsConcepts.monthOfTreatment); + return getObsFromEncounter(encounter, obsConcepts.monthOfTreatment); }, }, { key: 'adherence', header: t('adherence', 'Adherence'), getValue: (encounter) => { - return getObsFromEncounter(encounter, config.obsConcepts.adherenceAssessment); + return getObsFromEncounter(encounter, obsConcepts.adherenceAssessment); }, }, { key: 'adverseDrugReaction', header: t('adverseDrugReaction', 'Adverse Drug Reaction'), getValue: (encounter) => { - return getObsFromEncounter(encounter, config.obsConcepts.ADR); + return getObsFromEncounter(encounter, obsConcepts.ADR); }, }, { key: 'nextAppointment', header: t('nextAppointment', 'Next Appointment'), getValue: (encounter) => { - return getObsFromEncounter(encounter, config.obsConcepts.nextAppointmentDate, true); + return getObsFromEncounter(encounter, obsConcepts.nextAppointmentDate, true); }, }, ], @@ -201,11 +201,11 @@ const TBSummaryOverviewList: React.FC = ({ patientUuid }) => = ({ patientUuid }) => = ({ patientUuid }) => { const { t } = useTranslation(); - const config = useConfig(); const headerTitle = t('MdrTbEnrolment', 'TB/MDR TB Enrolment'); const [isEmptyOutcome, setIsEmptyOutcome] = useState(false); + const { formNames, formUuids, encounterTypes, obsConcepts } = useConfig(); useEffect(() => { - fetchPatientLastEncounter(patientUuid, config.encounterTypes.tbProgramEnrollment).then((encounter) => { - const result = encounter?.obs?.filter((ob) => ob?.concept?.uuid === config.obsConcepts.outcome); + fetchPatientLastEncounter(patientUuid, encounterTypes.tbProgramEnrollment).then((encounter) => { + const result = encounter?.obs?.filter((ob) => ob?.concept?.uuid === obsConcepts.outcome); if (result?.length === 0) { setIsEmptyOutcome(true); } @@ -33,21 +33,21 @@ const MdrTbList: React.FC = ({ patientUuid }) => { key: 'enrollmentDate', header: t('enrollmentDate', 'Enrollment Date'), getValue: (encounter) => { - return getObsFromEncounter(encounter, config.obsConcepts.enrollmentDate, true); + return getObsFromEncounter(encounter, obsConcepts.enrollmentDate, true); }, }, { key: 'caseID', header: t('caseID', 'Case ID'), getValue: (encounter) => { - return getObsFromEncounter(encounter, config.obsConcepts.caseID); + return getObsFromEncounter(encounter, obsConcepts.caseID); }, }, { key: 'tbTreatmentId', header: t('tbTreatmentId', 'TB Treatement ID'), getValue: (encounter) => { - const tBEnrollmentType = findObs(encounter, config.obsConcepts.tBEnrollmentType)?.value?.uuid; + const tBEnrollmentType = findObs(encounter, obsConcepts.tBEnrollmentType)?.value?.uuid; return getTbTreatmentId(encounter, tBEnrollmentType); }, }, @@ -55,7 +55,7 @@ const MdrTbList: React.FC = ({ patientUuid }) => { key: 'treatmentStartDate', header: t('treatmentStartDate', 'Treatment Start Date'), getValue: (encounter) => { - const tBEnrollmentType = findObs(encounter, config.obsConcepts.tBEnrollmentType)?.value?.uuid; + const tBEnrollmentType = findObs(encounter, obsConcepts.tBEnrollmentType)?.value?.uuid; return getTbTreatmentStartDate(encounter, tBEnrollmentType); }, }, @@ -63,7 +63,7 @@ const MdrTbList: React.FC = ({ patientUuid }) => { key: 'regimen', header: t('regimen', 'Regimen'), getValue: (encounter) => { - const tBEnrollmentType = findObs(encounter, config.obsConcepts.tBEnrollmentType)?.value?.uuid; + const tBEnrollmentType = findObs(encounter, obsConcepts.tBEnrollmentType)?.value?.uuid; return getTbRegimen(encounter, tBEnrollmentType); }, }, @@ -71,14 +71,14 @@ const MdrTbList: React.FC = ({ patientUuid }) => { key: 'treatmentOutcome', header: t('treatmentOutcome', 'Treatment outcome'), getValue: (encounter) => { - return getObsFromEncounter(encounter, config.obsConcepts.outcome); + return getObsFromEncounter(encounter, obsConcepts.outcome); }, }, { key: 'dateOfTreatmentOutcome', header: t('dateOfTreatmentOutcome', 'Date of Treatment Outcome'), getValue: (encounter) => { - return getObsFromEncounter(encounter, config.obsConcepts.DateOfTreatmentOutcome, true); + return getObsFromEncounter(encounter, obsConcepts.DateOfTreatmentOutcome, true); }, }, { @@ -108,8 +108,8 @@ const MdrTbList: React.FC = ({ patientUuid }) => { return ( = ({ patientUuid }) => { const { t } = useTranslation(); - const config = useConfig(); + const { formNames, formUuids, encounterTypes, obsConcepts } = useConfig(); const headerTitle = t('PatientTracing', 'Patient Tracing'); const columns: EncounterListColumn[] = useMemo( () => [ @@ -19,14 +19,14 @@ const TbPatientTracing: React.FC = ({ patientUuid }) => { key: 'contactDate', header: t('contactDate', 'Contact Date'), getValue: (encounter) => { - return getObsFromEncounter(encounter, config.obsConcepts.dateContacted, true); + return getObsFromEncounter(encounter, obsConcepts.dateContacted, true); }, }, { key: 'contactMethod', header: t('contactMethod', 'Contact Method'), getValue: (encounter) => { - return getObsFromEncounter(encounter, config.obsConcepts.modeOfContact); + return getObsFromEncounter(encounter, obsConcepts.modeOfContact); }, }, { @@ -56,8 +56,8 @@ const TbPatientTracing: React.FC = ({ patientUuid }) => { return ( = ({ patientUuid }) => { const { t } = useTranslation(); - const config = useConfig(); + const { formNames, formUuids, encounterTypes, obsConcepts } = useConfig(); const headerTitle = t('TbContactListing', 'TB Contact Listing'); const columns: EncounterListColumn[] = useMemo( @@ -20,14 +20,14 @@ const TbContactTracingList: React.FC = ({ patientUuid }) => { key: 'caseID', header: t('caseId', 'TB Case ID'), getValue: (encounter) => { - return getObsFromEncounter(encounter, config.obsConcepts.caseID); + return getObsFromEncounter(encounter, obsConcepts.caseID); }, }, { key: 'dateContactListed', header: t('dateContactListed', 'Date Contact Listed'), getValue: (encounter) => { - return getObsFromEncounter(encounter, config.obsConcepts.dateContacted, true); + return getObsFromEncounter(encounter, obsConcepts.dateContacted, true); }, }, { @@ -64,8 +64,8 @@ const TbContactTracingList: React.FC = ({ patientUuid }) => { return ( = ({ patientUuid }) => { const { t } = useTranslation(); - const { obsConcepts, encounterTypes } = useConfig(); + const { formNames, formUuids, encounterTypes, obsConcepts } = useConfig(); const headerRecentTPT = t('recentTptCases', 'Recent TPT Cases'); const headerPreviousTptCases = t('previousTptCases', 'Previous TPT Cases'); @@ -42,7 +42,7 @@ const TptPatientSummary: React.FC = ({ patientUuid }) => { key: 'tptIndication', header: t('indication', 'Indication'), encounterTypes: [encounterTypes.tptCaseEnrollment], - getObsValue: (encounter) => { + getObsValue: ([encounter]) => { return getObsFromEncounter(encounter, obsConcepts.tptIndication); }, }, @@ -50,7 +50,7 @@ const TptPatientSummary: React.FC = ({ patientUuid }) => { key: 'tptRegimen', header: t('regimen', 'Regimen'), encounterTypes: [encounterTypes.tptCaseEnrollment], - getObsValue: (encounter) => { + getObsValue: ([encounter]) => { return getObsFromEncounter(encounter, obsConcepts.tptRegimen); }, }, @@ -58,7 +58,7 @@ const TptPatientSummary: React.FC = ({ patientUuid }) => { key: 'tptAdherence', header: t('tptAdherence', 'Adherence'), encounterTypes: [encounterTypes.tptTreatmentAndFollowUp], - getObsValue: (encounter) => { + getObsValue: ([encounter]) => { return getObsFromEncounter(encounter, obsConcepts.tptAdherence); }, }, @@ -66,7 +66,7 @@ const TptPatientSummary: React.FC = ({ patientUuid }) => { key: 'tptAppointmentDate', header: t('nextAppointmentDate', 'Next Appointment Date'), encounterTypes: [encounterTypes.tptTreatmentAndFollowUp], - getObsValue: (encounter) => { + getObsValue: ([encounter]) => { return getObsFromEncounter(encounter, obsConcepts.tptAppointmentDate, true); }, }, @@ -97,7 +97,7 @@ const TptPatientSummary: React.FC = ({ patientUuid }) => { header: t('indication', 'Indication'), encounterTypes: [encounterTypes.tptCaseEnrollment], getValue: (encounter) => { - return getObsFromEncounter(encounter, obsConcepts.indication); + return getObsFromEncounter(encounter, obsConcepts.tptIndication); }, }, { @@ -128,6 +128,60 @@ const TptPatientSummary: React.FC = ({ patientUuid }) => { [], ); + const TptVisitsColumns: EncounterListColumn[] = useMemo( + () => [ + { + key: 'caseID', + header: t('caseID', 'Case ID'), + encounterTypes: [encounterTypes.tptCaseEnrollment], + getValue: (encounter) => { + return getObsFromEncounter(encounter, obsConcepts.tptTreatmentId); + }, + }, + { + key: 'visitDate', + header: t('visitDate', 'Visit Date'), + encounterTypes: [encounterTypes.tptCaseEnrollment], + getValue: (encounter) => { + return getObsFromEncounter(encounter, obsConcepts.tptEnrollmentDate, true); + }, + }, + { + key: 'tptIndication', + header: t('indication', 'Indication'), + encounterTypes: [encounterTypes.tptCaseEnrollment], + getValue: (encounter) => { + return getObsFromEncounter(encounter, obsConcepts.tptIndication); + }, + }, + { + key: 'regimen', + header: t('regimen', 'Regimen'), + encounterTypes: [encounterTypes.tptCaseEnrollment], + getValue: (encounter) => { + return getObsFromEncounter(encounter, obsConcepts.tptRegimen); + }, + }, + { + key: 'adherence', + header: t('adherence', 'Adherence'), + encounterTypes: [encounterTypes.tptTreatmentAndFollowUp], + getValue: (encounter) => { + return getObsFromEncounter(encounter, obsConcepts.tptAdherence); + }, + }, + { + key: 'nextAppointmentDate', + header: t('nextAppointmentDate', 'Next Appointment Date'), + encounterTypes: [encounterTypes.tptTreatmentAndFollowUp], + getValue: (encounter) => { + return getObsFromEncounter(encounter, obsConcepts.tptAppointmentDate, true); + }, + }, + ], + [], + ); + return ( <> = ({ patientUuid }) => { /> + - ); }; diff --git a/packages/esm-tb-app/src/views/tpt/program-management/tabs/tpt-enrolment.component.tsx b/packages/esm-tb-app/src/views/tpt/program-management/tabs/tpt-enrolment.component.tsx index 33d2b0a98..74fe248db 100644 --- a/packages/esm-tb-app/src/views/tpt/program-management/tabs/tpt-enrolment.component.tsx +++ b/packages/esm-tb-app/src/views/tpt/program-management/tabs/tpt-enrolment.component.tsx @@ -10,7 +10,7 @@ interface TptEnrolmentListProps { const TptEnrolmentList: React.FC = ({ patientUuid }) => { const { t } = useTranslation(); - const { obsConcepts, encounterTypes, formNames } = useConfig(); + const { obsConcepts, encounterTypes, formNames, formUuids } = useConfig(); const headerTitle = t('tptEnrolment', 'TPT Enrolment'); @@ -101,8 +101,8 @@ const TptEnrolmentList: React.FC = ({ patientUuid }) => { patientUuid={patientUuid} encounterType={encounterTypes.tptCaseEnrollment} formList={[ - { name: formNames.TptCaseEnrolmentFormName }, - { name: formNames.TptOutcomeFormName, excludedIntents: ['*'] }, + { name: formNames.TptCaseEnrolmentFormName, uuid: formUuids.tptCaseEnrolmentFormUuid }, + { name: formNames.TptOutcomeFormName, excludedIntents: ['*'], uuid: formUuids.tptOutcomeFormUuid }, ]} columns={columns} description={headerTitle} diff --git a/packages/esm-tb-app/src/views/tpt/program-management/tabs/tpt-treatment.component.tsx b/packages/esm-tb-app/src/views/tpt/program-management/tabs/tpt-treatment.component.tsx new file mode 100644 index 000000000..2e8888e3b --- /dev/null +++ b/packages/esm-tb-app/src/views/tpt/program-management/tabs/tpt-treatment.component.tsx @@ -0,0 +1,85 @@ +import React, { useMemo } from 'react'; +import { useTranslation } from 'react-i18next'; +import { useConfig } from '@openmrs/esm-framework'; +import { EncounterList, EncounterListColumn, getObsFromEncounter } from '@ohri/openmrs-esm-ohri-commons-lib'; +import { moduleName } from '../../../..'; + +interface TptTreatmentListProps { + patientUuid: string; +} + +const TptTreatmentList: React.FC = ({ patientUuid }) => { + const { t } = useTranslation(); + const { obsConcepts, encounterTypes, formNames, formUuids } = useConfig(); + const headerTitle = t('tptTreatment', 'TPT Treatment'); + const columns: EncounterListColumn[] = useMemo( + () => [ + { + key: 'caseId', + header: t('caseId', 'Case Id'), + getValue: (encounter: any) => { + return getObsFromEncounter(encounter, obsConcepts.caseID); + }, + }, + { + key: 'Adherence', + header: t('Adherence', 'Adherence'), + getValue: (encounter: any) => { + return getObsFromEncounter(encounter, obsConcepts.tptAdherence); + }, + }, + { + key: 'treatmentPlan', + header: t('treatmentPlan', 'Treatment Plan'), + getValue: (encounter: any) => { + return getObsFromEncounter(encounter, obsConcepts.treatmentPlan); + }, + }, + { + key: 'nextAppointment', + header: t('nextAppointment', 'Next Appointment'), + getValue: (encounter: any) => { + return getObsFromEncounter(encounter, obsConcepts.tptAppointmentDate, true); + }, + }, + { + key: 'actions', + header: t('actions', 'Actions'), + getValue: (encounter) => [ + { + form: { name: formNames.TptTreatmentFormName }, + encounterUuid: encounter.uuid, + intent: '*', + label: t('viewDetails', 'View Details'), + mode: 'view', + }, + { + form: { name: formNames.TptTreatmentFormName }, + encounterUuid: encounter.uuid, + intent: '*', + label: t('editForm', 'Edit Form'), + mode: 'edit', + }, + ], + }, + ], + [], + ); + + return ( + + ); +}; + +export default TptTreatmentList; diff --git a/packages/esm-tb-app/src/views/tpt/program-management/tpt-program-management.tsx b/packages/esm-tb-app/src/views/tpt/program-management/tpt-program-management.tsx index 113903bb7..8e391916c 100644 --- a/packages/esm-tb-app/src/views/tpt/program-management/tpt-program-management.tsx +++ b/packages/esm-tb-app/src/views/tpt/program-management/tpt-program-management.tsx @@ -2,7 +2,7 @@ import React from 'react'; import { Tabs, Tab, TabList, TabPanels, TabPanel } from '@carbon/react'; import styles from '../../common.scss'; import { useTranslation } from 'react-i18next'; -import TptTreatmentList from './tabs/tpt-treatment.component copy'; +import TptTreatmentList from './tabs/tpt-treatment.component'; import TptEnrolmentList from './tabs/tpt-enrolment.component'; import { PatientChartProps } from '@ohri/openmrs-esm-ohri-commons-lib'; diff --git a/packages/esm-tb-app/src/views/treatment-and-follow-up/tb-treatment-follow-up.component.tsx b/packages/esm-tb-app/src/views/treatment-and-follow-up/tb-treatment-follow-up.component.tsx index 099615a5c..d3b7d7900 100644 --- a/packages/esm-tb-app/src/views/treatment-and-follow-up/tb-treatment-follow-up.component.tsx +++ b/packages/esm-tb-app/src/views/treatment-and-follow-up/tb-treatment-follow-up.component.tsx @@ -11,7 +11,7 @@ import { useConfig } from '@openmrs/esm-framework'; const TbTreatmentFollowUpList: React.FC = ({ patientUuid }) => { const { t } = useTranslation(); - const config = useConfig(); + const { obsConcepts, encounterTypes, formNames, formUuids } = useConfig(); const headerTitle = t('TbTreatmentFollowUp', 'TB Follow-up'); const columns: EncounterListColumn[] = useMemo( @@ -20,35 +20,35 @@ const TbTreatmentFollowUpList: React.FC = ({ patientUuid }) = key: 'visitDate', header: t('visitDate', 'Visit Date'), getValue: (encounter) => { - return getObsFromEncounter(encounter, config.obsConcepts.visitDate, true); + return getObsFromEncounter(encounter, obsConcepts.visitDate, true); }, }, { key: 'caseId', header: t('caseId', 'Case ID'), getValue: (encounter) => { - return getObsFromEncounter(encounter, config.obsConcepts.followUpCaseId); + return getObsFromEncounter(encounter, obsConcepts.followUpCaseId); }, }, { key: 'monthOfTreatment', header: t('monthOfTreatment', 'Month of Rx'), getValue: (encounter) => { - return getObsFromEncounter(encounter, config.obsConcepts.monthOfTreatment); + return getObsFromEncounter(encounter, obsConcepts.monthOfTreatment); }, }, { key: 'adherenceAssessment', header: t('adherenceAssessment', 'Adherence'), getValue: (encounter) => { - return getObsFromEncounter(encounter, config.obsConcepts.adherenceAssessment); + return getObsFromEncounter(encounter, obsConcepts.adherenceAssessment); }, }, { key: 'nextAppointment', header: t('nextAppointment', 'Next Appointment'), getValue: (encounter) => { - return getObsFromEncounter(encounter, config.obsConcepts.nextAppointmentDate, true); + return getObsFromEncounter(encounter, obsConcepts.nextAppointmentDate, true); }, }, { @@ -78,8 +78,8 @@ const TbTreatmentFollowUpList: React.FC = ({ patientUuid }) = return ( , fsevents@patch:fsevents@npm%3A~2.3.2#optional!builtin": +"fsevents@patch:fsevents@npm%3A2.3.2#optional!builtin, fsevents@patch:fsevents@npm%3A^2.3.2#optional!builtin, fsevents@patch:fsevents@npm%3A~2.3.2#optional!builtin": version: 2.3.2 resolution: "fsevents@patch:fsevents@npm%3A2.3.2#optional!builtin::version=2.3.2&hash=df0bf1" dependencies: @@ -11266,7 +11342,7 @@ __metadata: languageName: node linkType: hard -"jest-coverage-badges@npm:1.1.2, jest-coverage-badges@npm:^1.0.0": +"jest-coverage-badges@npm:1.1.2": version: 1.1.2 resolution: "jest-coverage-badges@npm:1.1.2" dependencies: @@ -11976,6 +12052,16 @@ __metadata: languageName: node linkType: hard +"launch-editor@npm:^2.6.0": + version: 2.6.1 + resolution: "launch-editor@npm:2.6.1" + dependencies: + picocolors: "npm:^1.0.0" + shell-quote: "npm:^1.8.1" + checksum: e06d193075ac09f7f8109f10cabe464a211bf7ed4cbe75f83348d6f67bf4d9f162f06e7a1ab3e1cd7fc250b5342c3b57080618aff2e646dc34248fe499227601 + languageName: node + linkType: hard + "lazystream@npm:^1.0.0": version: 1.0.1 resolution: "lazystream@npm:1.0.1" @@ -13451,12 +13537,12 @@ __metadata: linkType: hard "openmrs@npm:next": - version: 5.3.3-pre.1395 - resolution: "openmrs@npm:5.3.3-pre.1395" + version: 5.4.1-pre.1592 + resolution: "openmrs@npm:5.4.1-pre.1592" dependencies: "@carbon/icons-react": "npm:11.26.0" - "@openmrs/esm-app-shell": "npm:5.3.3-pre.1395" - "@openmrs/webpack-config": "npm:5.3.3-pre.1395" + "@openmrs/esm-app-shell": "npm:5.4.1-pre.1592" + "@openmrs/webpack-config": "npm:5.4.1-pre.1592" "@pnpm/npm-conf": "npm:^2.1.0" "@swc/core": "npm:^1.3.58" autoprefixer: "npm:^10.4.2" @@ -13487,7 +13573,7 @@ __metadata: yargs: "npm:^17.6.2" bin: openmrs: ./dist/cli.js - checksum: 79e2b78bae6d5022f3656ab2f58b9f402a8d5429199267f7c526f73e3485a4ce27e00acc3c0376cf93e07608a64a5126afdfa2c7ad228eeb90f2d44ee24528b5 + checksum: 6a02311734010b6695d58a21785528b3ec168145cdd935c6949e93540e243b02ba6bb062927e1a99250643a7638fbc8eef620fd7ca6b0abf81473716f63bfcfe languageName: node linkType: hard @@ -13891,6 +13977,30 @@ __metadata: languageName: node linkType: hard +"playwright-core@npm:1.41.2": + version: 1.41.2 + resolution: "playwright-core@npm:1.41.2" + bin: + playwright-core: cli.js + checksum: 77ff881ebb9cc0654edd00c5ff202f5f61aee7a5318e1f12a82e30a3636de21e8b5982fae6138e5bb90115ae509c15a640cf85b10b3e2daebb2bb286da54fd4c + languageName: node + linkType: hard + +"playwright@npm:1.41.2": + version: 1.41.2 + resolution: "playwright@npm:1.41.2" + dependencies: + fsevents: "npm:2.3.2" + playwright-core: "npm:1.41.2" + dependenciesMeta: + fsevents: + optional: true + bin: + playwright: cli.js + checksum: 272399f622dc2df90fbef147b9b1cfab5d7a78cc364bdfa98d2bf08faa9894346f58629fe4fef41b108ca2cb203b3970d7886b7f392cb0399c75b521478e2920 + languageName: node + linkType: hard + "pngjs@npm:^3.0.0, pngjs@npm:^3.3.3": version: 3.4.0 resolution: "pngjs@npm:3.4.0" @@ -14702,13 +14812,6 @@ __metadata: languageName: node linkType: hard -"react-anchor-link-smooth-scroll@npm:^1.0.12": - version: 1.0.12 - resolution: "react-anchor-link-smooth-scroll@npm:1.0.12" - checksum: ca4f4a12eca0d657b5d1a96980b7b27fd94b8a896dc426d425364069f001d92c4c596936ba7b526aa26535623216e823792dee86470d5e798ef54110d5a0e0b8 - languageName: node - linkType: hard - "react-dom@npm:^18.1.0, react-dom@npm:^18.2.0": version: 18.2.0 resolution: "react-dom@npm:18.2.0" @@ -14827,20 +14930,7 @@ __metadata: languageName: node linkType: hard -"react-scroll@npm:^1.8.2": - version: 1.8.8 - resolution: "react-scroll@npm:1.8.8" - dependencies: - lodash.throttle: "npm:^4.1.1" - prop-types: "npm:^15.7.2" - peerDependencies: - react: ^15.5.4 || ^16.0.0 || ^17.0.0 || ^18.0.0 - react-dom: ^15.5.4 || ^16.0.0 || ^17.0.0 || ^18.0.0 - checksum: e15900dfaef77aa186e58a427e8128167c6293a06e2fe2fc06d230f6767c8e4b463d72211773de14fde91ae1402aa457e2b80fb4b4dd915330fb2bbc7d14ddb6 - languageName: node - linkType: hard - -"react-test-renderer@npm:^16.0.0-0, react-test-renderer@npm:^16.9.0": +"react-test-renderer@npm:^16.0.0-0": version: 16.14.0 resolution: "react-test-renderer@npm:16.14.0" dependencies: @@ -15779,6 +15869,13 @@ __metadata: languageName: node linkType: hard +"shell-quote@npm:^1.8.1": + version: 1.8.1 + resolution: "shell-quote@npm:1.8.1" + checksum: af19ab5a1ec30cb4b2f91fd6df49a7442d5c4825a2e269b3712eded10eedd7f9efeaab96d57829880733fc55bcdd8e9b1d8589b4befb06667c731d08145e274d + languageName: node + linkType: hard + "side-channel@npm:^1.0.4": version: 1.0.4 resolution: "side-channel@npm:1.0.4" @@ -17586,7 +17683,7 @@ __metadata: languageName: node linkType: hard -"webpack-dev-server@npm:^4.10.1, webpack-dev-server@npm:^4.8.1": +"webpack-dev-server@npm:^4.10.1": version: 4.11.1 resolution: "webpack-dev-server@npm:4.11.1" dependencies: @@ -17630,6 +17727,53 @@ __metadata: languageName: node linkType: hard +"webpack-dev-server@npm:^4.8.1": + version: 4.15.1 + resolution: "webpack-dev-server@npm:4.15.1" + dependencies: + "@types/bonjour": "npm:^3.5.9" + "@types/connect-history-api-fallback": "npm:^1.3.5" + "@types/express": "npm:^4.17.13" + "@types/serve-index": "npm:^1.9.1" + "@types/serve-static": "npm:^1.13.10" + "@types/sockjs": "npm:^0.3.33" + "@types/ws": "npm:^8.5.5" + ansi-html-community: "npm:^0.0.8" + bonjour-service: "npm:^1.0.11" + chokidar: "npm:^3.5.3" + colorette: "npm:^2.0.10" + compression: "npm:^1.7.4" + connect-history-api-fallback: "npm:^2.0.0" + default-gateway: "npm:^6.0.3" + express: "npm:^4.17.3" + graceful-fs: "npm:^4.2.6" + html-entities: "npm:^2.3.2" + http-proxy-middleware: "npm:^2.0.3" + ipaddr.js: "npm:^2.0.1" + launch-editor: "npm:^2.6.0" + open: "npm:^8.0.9" + p-retry: "npm:^4.5.0" + rimraf: "npm:^3.0.2" + schema-utils: "npm:^4.0.0" + selfsigned: "npm:^2.1.1" + serve-index: "npm:^1.9.1" + sockjs: "npm:^0.3.24" + spdy: "npm:^4.0.2" + webpack-dev-middleware: "npm:^5.3.1" + ws: "npm:^8.13.0" + peerDependencies: + webpack: ^4.37.0 || ^5.0.0 + peerDependenciesMeta: + webpack: + optional: true + webpack-cli: + optional: true + bin: + webpack-dev-server: bin/webpack-dev-server.js + checksum: fd6dfb6c71eb94696b21930ea4c2f25e95ba85fac1bbc15aa5d03af0a90712eba057901fa9131ed3e901665c95b2379208279aca61e9c48e7cda276c3caa95dd + languageName: node + linkType: hard + "webpack-merge@npm:^5.7.3": version: 5.8.0 resolution: "webpack-merge@npm:5.8.0" @@ -18355,7 +18499,7 @@ __metadata: languageName: node linkType: hard -"ws@npm:^8.11.0": +"ws@npm:^8.11.0, ws@npm:^8.13.0": version: 8.16.0 resolution: "ws@npm:8.16.0" peerDependencies: