@bathiran212/esm-patient-chart-app 7.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -0
- package/dist/108.js +1 -0
- package/dist/108.js.map +1 -0
- package/dist/1339.js +1 -0
- package/dist/1339.js.map +1 -0
- package/dist/1480.js +1 -0
- package/dist/1480.js.map +1 -0
- package/dist/1543.js +1 -0
- package/dist/1543.js.map +1 -0
- package/dist/1582.js +1 -0
- package/dist/1582.js.map +1 -0
- package/dist/1646.js +1 -0
- package/dist/1646.js.map +1 -0
- package/dist/1797.js +1 -0
- package/dist/1797.js.map +1 -0
- package/dist/1869.js +1 -0
- package/dist/1869.js.map +1 -0
- package/dist/1877.js +1 -0
- package/dist/1877.js.map +1 -0
- package/dist/2020.js +1 -0
- package/dist/2020.js.map +1 -0
- package/dist/2246.js +1 -0
- package/dist/2246.js.map +1 -0
- package/dist/2317.js +1 -0
- package/dist/2317.js.map +1 -0
- package/dist/2416.js +1 -0
- package/dist/2416.js.map +1 -0
- package/dist/2790.js +1 -0
- package/dist/2790.js.map +1 -0
- package/dist/282.js +1 -0
- package/dist/282.js.map +1 -0
- package/dist/2881.js +1 -0
- package/dist/2881.js.map +1 -0
- package/dist/3137.js +1 -0
- package/dist/3137.js.map +1 -0
- package/dist/3378.js +1 -0
- package/dist/3378.js.map +1 -0
- package/dist/3390.js +1 -0
- package/dist/3390.js.map +1 -0
- package/dist/3536.js +1 -0
- package/dist/3536.js.map +1 -0
- package/dist/3720.js +1 -0
- package/dist/3720.js.map +1 -0
- package/dist/3857.js +1 -0
- package/dist/3857.js.map +1 -0
- package/dist/3925.js +1 -0
- package/dist/3925.js.map +1 -0
- package/dist/3963.js +1 -0
- package/dist/3963.js.map +1 -0
- package/dist/3989.js +1 -0
- package/dist/3989.js.map +1 -0
- package/dist/4092.js +1 -0
- package/dist/4092.js.map +1 -0
- package/dist/4106.js +1 -0
- package/dist/4106.js.map +1 -0
- package/dist/4111.js +1 -0
- package/dist/4111.js.map +1 -0
- package/dist/4145.js +1 -0
- package/dist/4145.js.map +1 -0
- package/dist/434.js +1 -0
- package/dist/434.js.map +1 -0
- package/dist/4348.js +1 -0
- package/dist/4348.js.map +1 -0
- package/dist/4383.js +1 -0
- package/dist/4383.js.map +1 -0
- package/dist/4540.js +1 -0
- package/dist/4540.js.map +1 -0
- package/dist/4658.js +1 -0
- package/dist/4658.js.map +1 -0
- package/dist/466.js +1 -0
- package/dist/466.js.map +1 -0
- package/dist/4913.js +1 -0
- package/dist/4913.js.map +1 -0
- package/dist/4928.js +1 -0
- package/dist/4928.js.map +1 -0
- package/dist/5069.js +1 -0
- package/dist/5069.js.map +1 -0
- package/dist/5117.js +1 -0
- package/dist/5117.js.map +1 -0
- package/dist/5132.js +1 -0
- package/dist/5132.js.map +1 -0
- package/dist/5145.js +1 -0
- package/dist/5145.js.map +1 -0
- package/dist/52.js +1 -0
- package/dist/52.js.map +1 -0
- package/dist/5422.js +1 -0
- package/dist/5422.js.map +1 -0
- package/dist/5503.js +1 -0
- package/dist/5503.js.map +1 -0
- package/dist/5549.js +1 -0
- package/dist/5549.js.map +1 -0
- package/dist/556.js +1 -0
- package/dist/556.js.map +1 -0
- package/dist/5644.js +1 -0
- package/dist/5644.js.map +1 -0
- package/dist/5697.js +1 -0
- package/dist/5697.js.map +1 -0
- package/dist/5793.js +1 -0
- package/dist/5793.js.map +1 -0
- package/dist/5940.js +1 -0
- package/dist/5940.js.map +1 -0
- package/dist/5952.js +1 -0
- package/dist/5952.js.map +1 -0
- package/dist/6047.js +1 -0
- package/dist/6047.js.map +1 -0
- package/dist/6371.js +1 -0
- package/dist/6371.js.map +1 -0
- package/dist/6377.js +1 -0
- package/dist/6377.js.map +1 -0
- package/dist/6444.js +1 -0
- package/dist/6444.js.map +1 -0
- package/dist/6479.js +1 -0
- package/dist/6479.js.map +1 -0
- package/dist/6508.js +1 -0
- package/dist/6508.js.map +1 -0
- package/dist/6724.js +1 -0
- package/dist/6724.js.map +1 -0
- package/dist/6759.js +27 -0
- package/dist/6759.js.map +1 -0
- package/dist/689.js +1 -0
- package/dist/689.js.map +1 -0
- package/dist/6904.js +1 -0
- package/dist/6904.js.map +1 -0
- package/dist/7045.js +1 -0
- package/dist/7045.js.map +1 -0
- package/dist/7175.js +1 -0
- package/dist/7175.js.map +1 -0
- package/dist/7182.js +1 -0
- package/dist/7182.js.map +1 -0
- package/dist/7302.js +1 -0
- package/dist/7302.js.map +1 -0
- package/dist/7646.js +17 -0
- package/dist/7646.js.map +1 -0
- package/dist/7742.js +1 -0
- package/dist/7742.js.map +1 -0
- package/dist/7912.js +1 -0
- package/dist/7912.js.map +1 -0
- package/dist/8105.js +21 -0
- package/dist/8105.js.map +1 -0
- package/dist/8202.js +1 -0
- package/dist/8202.js.map +1 -0
- package/dist/8349.js +1 -0
- package/dist/8349.js.map +1 -0
- package/dist/8358.js +1 -0
- package/dist/8358.js.map +1 -0
- package/dist/8359.js +1 -0
- package/dist/8359.js.map +1 -0
- package/dist/8695.js +1 -0
- package/dist/8695.js.map +1 -0
- package/dist/8702.js +1 -0
- package/dist/8702.js.map +1 -0
- package/dist/8894.js +1 -0
- package/dist/8894.js.map +1 -0
- package/dist/8958.js +1 -0
- package/dist/8958.js.map +1 -0
- package/dist/903.js +1 -0
- package/dist/903.js.map +1 -0
- package/dist/9061.js +1 -0
- package/dist/9061.js.map +1 -0
- package/dist/9072.js +1 -0
- package/dist/9072.js.map +1 -0
- package/dist/9105.js +1 -0
- package/dist/9105.js.map +1 -0
- package/dist/9107.js +1 -0
- package/dist/9107.js.map +1 -0
- package/dist/9456.js +1 -0
- package/dist/9456.js.map +1 -0
- package/dist/9586.js +1 -0
- package/dist/9586.js.map +1 -0
- package/dist/9712.js +1 -0
- package/dist/9712.js.map +1 -0
- package/dist/9771.js +1 -0
- package/dist/9771.js.map +1 -0
- package/dist/9806.js +1 -0
- package/dist/9806.js.map +1 -0
- package/dist/9873.js +1 -0
- package/dist/9873.js.map +1 -0
- package/dist/9927.js +1 -0
- package/dist/9927.js.map +1 -0
- package/dist/main.js +6 -0
- package/dist/main.js.map +1 -0
- package/dist/openmrs-esm-patient-chart-app.js +6 -0
- package/dist/openmrs-esm-patient-chart-app.js.buildmanifest.json +2386 -0
- package/dist/openmrs-esm-patient-chart-app.js.map +1 -0
- package/dist/routes.json +1 -0
- package/package.json +63 -0
- package/rspack.config.js +1 -0
- package/src/actions-buttons/action-button.scss +3 -0
- package/src/actions-buttons/delete-visit.component.tsx +41 -0
- package/src/actions-buttons/delete-visit.test.tsx +26 -0
- package/src/actions-buttons/mark-patient-alive.component.tsx +42 -0
- package/src/actions-buttons/mark-patient-deceased.component.tsx +35 -0
- package/src/actions-buttons/start-visit.component.tsx +41 -0
- package/src/actions-buttons/start-visit.test.tsx +44 -0
- package/src/actions-buttons/stop-visit.component.tsx +39 -0
- package/src/actions-buttons/stop-visit.test.tsx +27 -0
- package/src/clinical-views/encounter-list/encounter-list-tabs.extension.tsx +78 -0
- package/src/clinical-views/encounter-list/encounter-list-tabs.scss +7 -0
- package/src/clinical-views/encounter-list/encounter-list.component.tsx +306 -0
- package/src/clinical-views/encounter-list/encounter-list.scss +36 -0
- package/src/clinical-views/encounter-list/table.component.tsx +63 -0
- package/src/clinical-views/encounter-list/table.scss +11 -0
- package/src/clinical-views/encounter-list/tag.component.test.tsx +307 -0
- package/src/clinical-views/encounter-list/tag.component.tsx +43 -0
- package/src/clinical-views/encounter-tile/clinical-views-summary.component.tsx +40 -0
- package/src/clinical-views/encounter-tile/encounter-tile.component.tsx +94 -0
- package/src/clinical-views/encounter-tile/tile.scss +82 -0
- package/src/clinical-views/hooks/index.ts +3 -0
- package/src/clinical-views/hooks/useEncounterRows.ts +60 -0
- package/src/clinical-views/hooks/useEncountersByVisit.ts +13 -0
- package/src/clinical-views/hooks/useFormsJson.ts +15 -0
- package/src/clinical-views/hooks/useLastEncounter.ts +29 -0
- package/src/clinical-views/types.ts +305 -0
- package/src/clinical-views/utils/concept-utils.ts +24 -0
- package/src/clinical-views/utils/encounter-list-config-builder.ts +160 -0
- package/src/clinical-views/utils/encounter-list.resource.ts +26 -0
- package/src/clinical-views/utils/helpers.ts +226 -0
- package/src/clinical-views/utils/index.ts +90 -0
- package/src/config-schema.ts +235 -0
- package/src/constants.ts +11 -0
- package/src/dashboard.meta.ts +15 -0
- package/src/data.resource.ts +117 -0
- package/src/declarations.d.ts +4 -0
- package/src/index.ts +204 -0
- package/src/loader/loader.component.tsx +11 -0
- package/src/loader/loader.scss +9 -0
- package/src/mark-patient-alive/mark-patient-alive.modal.tsx +54 -0
- package/src/mark-patient-deceased/mark-patient-deceased-form.scss +175 -0
- package/src/mark-patient-deceased/mark-patient-deceased-form.test.tsx +203 -0
- package/src/mark-patient-deceased/mark-patient-deceased-form.workspace.tsx +295 -0
- package/src/offline.ts +41 -0
- package/src/patient-banner-tags/visit-attribute-tags.extension.tsx +62 -0
- package/src/patient-banner-tags/visit-attribute-tags.scss +8 -0
- package/src/patient-chart/chart-review/chart-review.component.tsx +138 -0
- package/src/patient-chart/chart-review/chart-review.test.tsx +77 -0
- package/src/patient-chart/chart-review/dashboard-view.component.tsx +85 -0
- package/src/patient-chart/chart-review/dashboard-view.scss +84 -0
- package/src/patient-chart/patient-chart.component.tsx +71 -0
- package/src/patient-chart/patient-chart.resources.test.ts +238 -0
- package/src/patient-chart/patient-chart.resources.ts +231 -0
- package/src/patient-chart/patient-chart.scss +65 -0
- package/src/patient-details-tile/patient-details-tile.component.tsx +25 -0
- package/src/patient-details-tile/patient-details-tile.scss +24 -0
- package/src/root.component.tsx +35 -0
- package/src/root.scss +54 -0
- package/src/routes.json +267 -0
- package/src/side-nav/side-menu.component.tsx +10 -0
- package/src/side-nav/side-menu.scss +38 -0
- package/src/side-nav/side-menu.test.tsx +27 -0
- package/src/utils.test.ts +17 -0
- package/src/utils.ts +5 -0
- package/src/visit/hooks/useDefaultFacilityLocation.tsx +15 -0
- package/src/visit/hooks/useDefaultVisitLocation.tsx +24 -0
- package/src/visit/hooks/useDeleteVisit.test.tsx +267 -0
- package/src/visit/hooks/useDeleteVisit.tsx +103 -0
- package/src/visit/hooks/useOfflineVisitType.tsx +18 -0
- package/src/visit/hooks/useRecommendedVisitTypes.tsx +34 -0
- package/src/visit/hooks/useVisitAttributeType.tsx +102 -0
- package/src/visit/start-visit-button.component.tsx +47 -0
- package/src/visit/start-visit-button.test.tsx +32 -0
- package/src/visit/visit-action-items/delete-visit-action-item.component.tsx +60 -0
- package/src/visit/visit-action-items/delete-visit-action-item.test.tsx +48 -0
- package/src/visit/visit-action-items/edit-visit-details.component.tsx +79 -0
- package/src/visit/visit-form/base-visit-type.component.tsx +121 -0
- package/src/visit/visit-form/base-visit-type.scss +75 -0
- package/src/visit/visit-form/base-visit-type.test.tsx +153 -0
- package/src/visit/visit-form/exported-visit-form.workspace.tsx +755 -0
- package/src/visit/visit-form/location-selector.component.tsx +86 -0
- package/src/visit/visit-form/location-selector.test.tsx +146 -0
- package/src/visit/visit-form/recommended-visit-type.component.tsx +32 -0
- package/src/visit/visit-form/visit-attribute-type.component.tsx +258 -0
- package/src/visit/visit-form/visit-attribute-type.scss +5 -0
- package/src/visit/visit-form/visit-date-time.component.tsx +206 -0
- package/src/visit/visit-form/visit-form.resource.ts +401 -0
- package/src/visit/visit-form/visit-form.scss +167 -0
- package/src/visit/visit-form/visit-form.test.tsx +1233 -0
- package/src/visit/visit-form/visit-form.workspace.tsx +61 -0
- package/src/visit/visit-form/visit-type.test.tsx +88 -0
- package/src/visit/visit-history-table/visit-actions-cell.component.tsx +20 -0
- package/src/visit/visit-history-table/visit-actions-cell.scss +4 -0
- package/src/visit/visit-history-table/visit-date-cell.component.tsx +19 -0
- package/src/visit/visit-history-table/visit-diagnoses-cell.component.tsx +18 -0
- package/src/visit/visit-history-table/visit-history-table.component.tsx +145 -0
- package/src/visit/visit-history-table/visit-history-table.scss +25 -0
- package/src/visit/visit-history-table/visit-type-cell.component.tsx +15 -0
- package/src/visit/visit-prompt/delete-visit-dialog.modal.tsx +46 -0
- package/src/visit/visit-prompt/delete-visit-dialog.test.tsx +79 -0
- package/src/visit/visit-prompt/end-visit-dialog.modal.tsx +82 -0
- package/src/visit/visit-prompt/end-visit-dialog.scss +7 -0
- package/src/visit/visit-prompt/end-visit-dialog.test.tsx +131 -0
- package/src/visit/visit-prompt/modify-visit-date.modal.tsx +40 -0
- package/src/visit/visit-prompt/start-visit-dialog.modal.tsx +64 -0
- package/src/visit/visit-prompt/start-visit-dialog.scss +10 -0
- package/src/visit/visit-prompt/start-visit-dialog.test.tsx +40 -0
- package/src/visit/visits-widget/active-visit-buttons/active-visit-buttons.scss +7 -0
- package/src/visit/visits-widget/active-visit-buttons/active-visit-buttons.tsx +178 -0
- package/src/visit/visits-widget/current-visit-summary.extension.tsx +48 -0
- package/src/visit/visits-widget/current-visit-summary.scss +10 -0
- package/src/visit/visits-widget/current-visit-summary.test.tsx +85 -0
- package/src/visit/visits-widget/encounter-observations/encounter-observations.component.tsx +67 -0
- package/src/visit/visits-widget/encounter-observations/index.ts +3 -0
- package/src/visit/visits-widget/encounter-observations/styles.scss +22 -0
- package/src/visit/visits-widget/past-visits-components/delete-encounter.modal.tsx +47 -0
- package/src/visit/visits-widget/past-visits-components/delete-encounter.scss +9 -0
- package/src/visit/visits-widget/past-visits-components/encounters-table/all-encounters-table.component.tsx +49 -0
- package/src/visit/visits-widget/past-visits-components/encounters-table/completed-forms-table.component.tsx +67 -0
- package/src/visit/visits-widget/past-visits-components/encounters-table/completed-forms-table.test.tsx +146 -0
- package/src/visit/visits-widget/past-visits-components/encounters-table/encounters-table.component.tsx +452 -0
- package/src/visit/visits-widget/past-visits-components/encounters-table/encounters-table.resource.test.ts +156 -0
- package/src/visit/visits-widget/past-visits-components/encounters-table/encounters-table.resource.ts +215 -0
- package/src/visit/visits-widget/past-visits-components/encounters-table/encounters-table.scss +113 -0
- package/src/visit/visits-widget/past-visits-components/encounters-table/encounters-table.test.tsx +432 -0
- package/src/visit/visits-widget/past-visits-components/encounters-table/visit-completed-forms-table.component.tsx +61 -0
- package/src/visit/visits-widget/past-visits-components/encounters-table/visit-completed-forms-table.test.tsx +125 -0
- package/src/visit/visits-widget/past-visits-components/encounters-table/visit-encounters-table.component.tsx +47 -0
- package/src/visit/visits-widget/past-visits-components/medications-summary.component.tsx +163 -0
- package/src/visit/visits-widget/past-visits-components/notes-summary.component.tsx +66 -0
- package/src/visit/visits-widget/past-visits-components/patient-notes-summary.component.tsx +318 -0
- package/src/visit/visits-widget/past-visits-components/tests-summary.component.tsx +16 -0
- package/src/visit/visits-widget/past-visits-components/visit-summary.component.tsx +192 -0
- package/src/visit/visits-widget/past-visits-components/visit-summary.scss +72 -0
- package/src/visit/visits-widget/past-visits-components/visit-summary.test.tsx +105 -0
- package/src/visit/visits-widget/single-visit-details/visit-timeline/visit-timeline.component.tsx +94 -0
- package/src/visit/visits-widget/single-visit-details/visit-timeline/visit-timeline.scss +60 -0
- package/src/visit/visits-widget/visit-context/retrospective-data-date-time-picker/restrospective-date-time-picker.scss +35 -0
- package/src/visit/visits-widget/visit-context/retrospective-data-date-time-picker/retrospective-date-time-picker.component.tsx +140 -0
- package/src/visit/visits-widget/visit-context/visit-context-header.extension.tsx +61 -0
- package/src/visit/visits-widget/visit-context/visit-context-header.scss +45 -0
- package/src/visit/visits-widget/visit-context/visit-context-header.test.tsx +59 -0
- package/src/visit/visits-widget/visit-context/visit-context-info.component.tsx +37 -0
- package/src/visit/visits-widget/visit-context/visit-context-info.scss +12 -0
- package/src/visit/visits-widget/visit-context/visit-context-switcher.modal.tsx +166 -0
- package/src/visit/visits-widget/visit-context/visit-context-switcher.scss +83 -0
- package/src/visit/visits-widget/visit-context/visit-context-switcher.test.tsx +79 -0
- package/src/visit/visits-widget/visit-detail-overview.component.tsx +67 -0
- package/src/visit/visits-widget/visit-detail-overview.scss +301 -0
- package/src/visit/visits-widget/visit-detail-overview.test.tsx +205 -0
- package/src/visit/visits-widget/visit.resource.tsx +146 -0
- package/translations/am.json +209 -0
- package/translations/ar.json +209 -0
- package/translations/ar_SY.json +209 -0
- package/translations/bn.json +209 -0
- package/translations/cs.json +209 -0
- package/translations/de.json +209 -0
- package/translations/en.json +209 -0
- package/translations/en_US.json +209 -0
- package/translations/es.json +209 -0
- package/translations/es_MX.json +209 -0
- package/translations/fr.json +209 -0
- package/translations/he.json +209 -0
- package/translations/hi.json +209 -0
- package/translations/hi_IN.json +209 -0
- package/translations/id.json +209 -0
- package/translations/it.json +209 -0
- package/translations/ka.json +209 -0
- package/translations/km.json +209 -0
- package/translations/ku.json +209 -0
- package/translations/ky.json +209 -0
- package/translations/lg.json +209 -0
- package/translations/ne.json +209 -0
- package/translations/pl.json +209 -0
- package/translations/pt.json +209 -0
- package/translations/pt_BR.json +209 -0
- package/translations/qu.json +209 -0
- package/translations/ro_RO.json +209 -0
- package/translations/ru_RU.json +209 -0
- package/translations/si.json +209 -0
- package/translations/sq.json +209 -0
- package/translations/sw.json +209 -0
- package/translations/sw_KE.json +209 -0
- package/translations/tr.json +209 -0
- package/translations/tr_TR.json +209 -0
- package/translations/uk.json +209 -0
- package/translations/uz.json +209 -0
- package/translations/uz@Latn.json +209 -0
- package/translations/uz_UZ.json +209 -0
- package/translations/vi.json +209 -0
- package/translations/zh.json +209 -0
- package/translations/zh_CN.json +209 -0
- package/translations/zh_TW.json +209 -0
- package/tsconfig.json +4 -0
- package/vitest.config.ts +4 -0
|
@@ -0,0 +1,1233 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { vi, describe, it, expect, test, beforeEach } from 'vitest';
|
|
3
|
+
import dayjs from 'dayjs';
|
|
4
|
+
import { fireEvent, render, renderHook, screen, waitFor } from '@testing-library/react';
|
|
5
|
+
import userEvent from '@testing-library/user-event';
|
|
6
|
+
import {
|
|
7
|
+
type FetchResponse,
|
|
8
|
+
getDefaultsFromConfigSchema,
|
|
9
|
+
saveVisit,
|
|
10
|
+
showSnackbar,
|
|
11
|
+
updateVisit,
|
|
12
|
+
useConfig,
|
|
13
|
+
useEmrConfiguration,
|
|
14
|
+
useLocations,
|
|
15
|
+
useVisit,
|
|
16
|
+
useVisitTypes,
|
|
17
|
+
Workspace2,
|
|
18
|
+
type Visit,
|
|
19
|
+
} from '@openmrs/esm-framework';
|
|
20
|
+
import { mockLocations, mockPastVisitWithEncounters, mockVisitTypes, mockVisitWithAttributes } from '__mocks__';
|
|
21
|
+
import { mockPatient } from 'tools';
|
|
22
|
+
import { type ChartConfig, esmPatientChartSchema } from '../../config-schema';
|
|
23
|
+
import { useVisitAttributeType } from '../hooks/useVisitAttributeType';
|
|
24
|
+
import {
|
|
25
|
+
computeEarliestAllowedStartDate,
|
|
26
|
+
convertToDateTimeFields,
|
|
27
|
+
createVisitAttribute,
|
|
28
|
+
deleteVisitAttribute,
|
|
29
|
+
updateVisitAttribute,
|
|
30
|
+
useAllowOverlappingVisits,
|
|
31
|
+
useEarliestAllowedVisitStartDate,
|
|
32
|
+
useVisitFormCallbacks,
|
|
33
|
+
useVisitFormSchemaAndDefaultValues,
|
|
34
|
+
} from './visit-form.resource';
|
|
35
|
+
import VisitForm, { type VisitFormProps } from './visit-form.workspace';
|
|
36
|
+
import { type PatientWorkspace2DefinitionProps } from '@openmrs/esm-patient-common-lib/src';
|
|
37
|
+
|
|
38
|
+
const visitUuid = 'test_visit_uuid';
|
|
39
|
+
const visitAttributes = {
|
|
40
|
+
punctuality: {
|
|
41
|
+
uuid: '57ea0cbb-064f-4d09-8cf4-e8228700491c',
|
|
42
|
+
name: 'Punctuality',
|
|
43
|
+
display: 'Punctuality',
|
|
44
|
+
datatypeClassname: 'org.openmrs.customdatatype.datatype.ConceptDatatype' as const,
|
|
45
|
+
datatypeConfig: '',
|
|
46
|
+
preferredHandlerClassname: 'default',
|
|
47
|
+
description: '',
|
|
48
|
+
retired: false,
|
|
49
|
+
},
|
|
50
|
+
insurancePolicyNumber: {
|
|
51
|
+
uuid: 'aac48226-d143-4274-80e0-264db4e368ee',
|
|
52
|
+
name: 'Insurance Policy Number',
|
|
53
|
+
display: 'Insurance Policy Number',
|
|
54
|
+
datatypeConfig: '',
|
|
55
|
+
datatypeClassname: 'org.openmrs.customdatatype.datatype.FreeTextDatatype',
|
|
56
|
+
description: '',
|
|
57
|
+
preferredHandlerClassname: 'default',
|
|
58
|
+
retired: false,
|
|
59
|
+
},
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
const mockCloseWorkspace = vi.fn();
|
|
63
|
+
const mockMutateVisitContext = vi.fn();
|
|
64
|
+
const defaultProps: PatientWorkspace2DefinitionProps<VisitFormProps, {}> = {
|
|
65
|
+
closeWorkspace: mockCloseWorkspace,
|
|
66
|
+
workspaceProps: {
|
|
67
|
+
openedFrom: 'test',
|
|
68
|
+
},
|
|
69
|
+
windowProps: {},
|
|
70
|
+
groupProps: {
|
|
71
|
+
patientUuid: mockPatient.id,
|
|
72
|
+
patient: mockPatient,
|
|
73
|
+
visitContext: null,
|
|
74
|
+
mutateVisitContext: mockMutateVisitContext,
|
|
75
|
+
},
|
|
76
|
+
workspaceName: '',
|
|
77
|
+
launchChildWorkspace: vi.fn(),
|
|
78
|
+
windowName: '',
|
|
79
|
+
isRootWorkspace: false,
|
|
80
|
+
showActionMenu: true,
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
const defaultVisitLocation = {
|
|
84
|
+
display: 'Outpatient Clinic',
|
|
85
|
+
uuid: 'location-a',
|
|
86
|
+
};
|
|
87
|
+
const mockUseDefaultVisitLocation = vi.fn().mockReturnValue(defaultVisitLocation);
|
|
88
|
+
|
|
89
|
+
const mockSaveVisit = vi.mocked(saveVisit);
|
|
90
|
+
const mockUpdateVisit = vi.mocked(updateVisit);
|
|
91
|
+
const mockWorkspace2 = vi.mocked(Workspace2);
|
|
92
|
+
const mockUseConfig = vi.mocked(useConfig<ChartConfig>);
|
|
93
|
+
const mockUseVisitAttributeType = vi.mocked(useVisitAttributeType);
|
|
94
|
+
const mockUseVisit = vi.mocked(useVisit);
|
|
95
|
+
const mockUseVisitTypes = vi.mocked(useVisitTypes);
|
|
96
|
+
const mockUseLocations = vi.mocked(useLocations);
|
|
97
|
+
const mockUseEmrConfiguration = vi.mocked(useEmrConfiguration);
|
|
98
|
+
|
|
99
|
+
// from ./visit-form.resource
|
|
100
|
+
const mockOnVisitCreatedOrUpdatedCallback = vi.fn();
|
|
101
|
+
vi.mocked(useVisitFormCallbacks).mockReturnValue([
|
|
102
|
+
new Map([['test-extension-id', { onVisitCreatedOrUpdated: mockOnVisitCreatedOrUpdatedCallback }]]), // visitFormCallbacks
|
|
103
|
+
vi.fn(), // setVisitFormCallbacks
|
|
104
|
+
]);
|
|
105
|
+
const mockCreateVisitAttribute = vi.mocked(createVisitAttribute).mockResolvedValue({} as unknown as FetchResponse);
|
|
106
|
+
const mockUpdateVisitAttribute = vi.mocked(updateVisitAttribute).mockResolvedValue({} as unknown as FetchResponse);
|
|
107
|
+
const mockDeleteVisitAttribute = vi.mocked(deleteVisitAttribute).mockResolvedValue({} as unknown as FetchResponse);
|
|
108
|
+
|
|
109
|
+
vi.mock('@openmrs/esm-patient-common-lib', async () => ({
|
|
110
|
+
...((await vi.importActual('@openmrs/esm-patient-common-lib')) as object),
|
|
111
|
+
useActivePatientEnrollment: vi.fn().mockReturnValue({
|
|
112
|
+
activePatientEnrollment: [],
|
|
113
|
+
isLoading: false,
|
|
114
|
+
}),
|
|
115
|
+
}));
|
|
116
|
+
|
|
117
|
+
vi.mock('../hooks/useVisitAttributeType', () => ({
|
|
118
|
+
useVisitAttributeType: vi.fn((attributeUuid) => {
|
|
119
|
+
if (attributeUuid === visitAttributes.punctuality.uuid) {
|
|
120
|
+
return {
|
|
121
|
+
isLoading: false,
|
|
122
|
+
error: null,
|
|
123
|
+
data: visitAttributes.punctuality,
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
if (attributeUuid === visitAttributes.insurancePolicyNumber.uuid) {
|
|
127
|
+
return {
|
|
128
|
+
isLoading: false,
|
|
129
|
+
error: null,
|
|
130
|
+
data: visitAttributes.insurancePolicyNumber,
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
}),
|
|
134
|
+
useVisitAttributeTypes: vi.fn(() => ({
|
|
135
|
+
isLoading: false,
|
|
136
|
+
error: null,
|
|
137
|
+
visitAttributeTypes: [visitAttributes.punctuality, visitAttributes.insurancePolicyNumber],
|
|
138
|
+
})),
|
|
139
|
+
useConceptAnswersForVisitAttributeType: vi.fn(() => ({
|
|
140
|
+
isLoading: false,
|
|
141
|
+
error: null,
|
|
142
|
+
answers: [
|
|
143
|
+
{
|
|
144
|
+
uuid: '66cdc0a1-aa19-4676-af51-80f66d78d9eb',
|
|
145
|
+
display: 'On time',
|
|
146
|
+
links: [
|
|
147
|
+
{
|
|
148
|
+
rel: 'self',
|
|
149
|
+
uri: 'http://localhost:8080/openmrs/ws/rest/v1/concept/66cdc0a1-aa19-4676-af51-80f66d78d9eb',
|
|
150
|
+
resourceAlias: 'concept',
|
|
151
|
+
},
|
|
152
|
+
],
|
|
153
|
+
},
|
|
154
|
+
{
|
|
155
|
+
uuid: '66cdc0a1-aa19-4676-af51-80f66d78d9ec',
|
|
156
|
+
display: 'Late',
|
|
157
|
+
links: [
|
|
158
|
+
{
|
|
159
|
+
rel: 'self',
|
|
160
|
+
uri: 'http://localhost:8080/openmrs/ws/rest/v1/concept/66cdc0a1-aa19-4676-af51-80f66d78d9ec',
|
|
161
|
+
resourceAlias: 'concept',
|
|
162
|
+
},
|
|
163
|
+
],
|
|
164
|
+
},
|
|
165
|
+
],
|
|
166
|
+
})),
|
|
167
|
+
}));
|
|
168
|
+
|
|
169
|
+
vi.mock('../hooks/useDefaultFacilityLocation', async () => {
|
|
170
|
+
const requireActual = (await vi.importActual('../hooks/useDefaultFacilityLocation')) as object;
|
|
171
|
+
|
|
172
|
+
return {
|
|
173
|
+
...requireActual,
|
|
174
|
+
useDefaultFacilityLocation: vi.fn(() => ({
|
|
175
|
+
defaultFacility: null,
|
|
176
|
+
isLoading: false,
|
|
177
|
+
error: null,
|
|
178
|
+
})),
|
|
179
|
+
};
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
vi.mock('../hooks/useDefaultVisitLocation', async () => {
|
|
183
|
+
const requireActual = (await vi.importActual('../hooks/useDefaultVisitLocation')) as object;
|
|
184
|
+
|
|
185
|
+
return {
|
|
186
|
+
...requireActual,
|
|
187
|
+
useDefaultVisitLocation: vi.fn((...args) => mockUseDefaultVisitLocation(...args)),
|
|
188
|
+
};
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
vi.mock('./visit-form.resource', async () => {
|
|
192
|
+
const requireActual = (await vi.importActual('./visit-form.resource')) as object;
|
|
193
|
+
return {
|
|
194
|
+
...requireActual,
|
|
195
|
+
useAllowOverlappingVisits: vi.fn(),
|
|
196
|
+
useVisitFormCallbacks: vi.fn(),
|
|
197
|
+
useEarliestAllowedVisitStartDate: vi.fn(),
|
|
198
|
+
createVisitAttribute: vi.fn(),
|
|
199
|
+
updateVisitAttribute: vi.fn(),
|
|
200
|
+
deleteVisitAttribute: vi.fn(),
|
|
201
|
+
};
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
const mockUseEarliestAllowedVisitStartDate = vi.mocked(useEarliestAllowedVisitStartDate);
|
|
205
|
+
mockUseEarliestAllowedVisitStartDate.mockReturnValue({ earliestAllowedStartDate: null, isLoading: false });
|
|
206
|
+
|
|
207
|
+
const mockUseAllowOverlappingVisits = vi.mocked(useAllowOverlappingVisits);
|
|
208
|
+
mockUseAllowOverlappingVisits.mockReturnValue({ allowOverlappingVisits: true, isLoading: false });
|
|
209
|
+
|
|
210
|
+
mockSaveVisit.mockResolvedValue({
|
|
211
|
+
status: 201,
|
|
212
|
+
data: {
|
|
213
|
+
uuid: visitUuid,
|
|
214
|
+
visitType: {
|
|
215
|
+
display: 'Facility Visit',
|
|
216
|
+
},
|
|
217
|
+
},
|
|
218
|
+
} as unknown as FetchResponse<Visit>);
|
|
219
|
+
|
|
220
|
+
describe('Visit form', () => {
|
|
221
|
+
beforeEach(() => {
|
|
222
|
+
mockUseConfig.mockReturnValue({
|
|
223
|
+
...getDefaultsFromConfigSchema(esmPatientChartSchema),
|
|
224
|
+
visitAttributeTypes: [
|
|
225
|
+
{
|
|
226
|
+
uuid: visitAttributes.punctuality.uuid,
|
|
227
|
+
required: false,
|
|
228
|
+
displayInThePatientBanner: true,
|
|
229
|
+
},
|
|
230
|
+
{
|
|
231
|
+
uuid: visitAttributes.insurancePolicyNumber.uuid,
|
|
232
|
+
required: false,
|
|
233
|
+
displayInThePatientBanner: true,
|
|
234
|
+
},
|
|
235
|
+
],
|
|
236
|
+
});
|
|
237
|
+
mockUseVisitTypes.mockReturnValue(mockVisitTypes);
|
|
238
|
+
mockUseLocations.mockReturnValue(mockLocations);
|
|
239
|
+
mockUseEmrConfiguration.mockReturnValue({
|
|
240
|
+
emrConfiguration: {
|
|
241
|
+
atFacilityVisitType: null,
|
|
242
|
+
},
|
|
243
|
+
isLoadingEmrConfiguration: false,
|
|
244
|
+
errorFetchingEmrConfiguration: null,
|
|
245
|
+
mutateEmrConfiguration: null,
|
|
246
|
+
});
|
|
247
|
+
mockUseDefaultVisitLocation.mockReset();
|
|
248
|
+
mockUseDefaultVisitLocation.mockReturnValue(defaultVisitLocation);
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
it('renders the Start Visit form with all the relevant fields and values', async () => {
|
|
252
|
+
const user = userEvent.setup();
|
|
253
|
+
renderVisitForm();
|
|
254
|
+
|
|
255
|
+
// ===================
|
|
256
|
+
// Testing the visit status content switcher and how they show/hide the visit start/end fields
|
|
257
|
+
const visitStatusNew = screen.getByRole('tab', { name: /new/i });
|
|
258
|
+
const visitStatusOngoing = screen.getByRole('tab', { name: /ongoing/i });
|
|
259
|
+
const visitStatusPast = screen.getByRole('tab', { name: /in the past/i });
|
|
260
|
+
expect(visitStatusNew).toBeInTheDocument();
|
|
261
|
+
expect(visitStatusOngoing).toBeInTheDocument();
|
|
262
|
+
expect(visitStatusPast).toBeInTheDocument();
|
|
263
|
+
|
|
264
|
+
const visitStartDate = () => screen.queryByLabelText(/start date/i);
|
|
265
|
+
const visitStartTime = () => screen.queryByRole('textbox', { name: /start time/i });
|
|
266
|
+
const visitStartTimeFormat = () => screen.queryByRole('combobox', { name: /start time format/i });
|
|
267
|
+
const visitEndDate = () => screen.queryByLabelText(/end date/i);
|
|
268
|
+
const visitEndTime = () => screen.queryByRole('textbox', { name: /end time/i });
|
|
269
|
+
const visitEndTimeFormat = () => screen.queryByRole('combobox', { name: /end time format/i });
|
|
270
|
+
|
|
271
|
+
// when visit status is new, no start date / end date fields
|
|
272
|
+
await user.click(visitStatusNew);
|
|
273
|
+
expect(visitStartDate()).not.toBeInTheDocument();
|
|
274
|
+
expect(visitStartTime()).not.toBeInTheDocument();
|
|
275
|
+
expect(visitStartTimeFormat()).not.toBeInTheDocument();
|
|
276
|
+
expect(visitEndDate()).not.toBeInTheDocument();
|
|
277
|
+
expect(visitEndTime()).not.toBeInTheDocument();
|
|
278
|
+
expect(visitEndTimeFormat()).not.toBeInTheDocument();
|
|
279
|
+
|
|
280
|
+
// when visit status is ongoing, should have only start date fields
|
|
281
|
+
await visitStatusOngoing.click();
|
|
282
|
+
expect(visitStartDate()).toBeInTheDocument();
|
|
283
|
+
expect(visitStartTime()).toBeInTheDocument();
|
|
284
|
+
expect(visitStartTimeFormat()).toBeInTheDocument();
|
|
285
|
+
expect(visitEndDate()).not.toBeInTheDocument();
|
|
286
|
+
expect(visitEndTime()).not.toBeInTheDocument();
|
|
287
|
+
expect(visitEndTimeFormat()).not.toBeInTheDocument();
|
|
288
|
+
|
|
289
|
+
// when visit status is past, should have both start date and end date fields
|
|
290
|
+
await visitStatusPast.click();
|
|
291
|
+
expect(visitStartDate()).toBeInTheDocument();
|
|
292
|
+
expect(visitStartTime()).toBeInTheDocument();
|
|
293
|
+
expect(visitStartTimeFormat()).toBeInTheDocument();
|
|
294
|
+
expect(visitEndDate()).toBeInTheDocument();
|
|
295
|
+
expect(visitEndTime()).toBeInTheDocument();
|
|
296
|
+
expect(visitEndTimeFormat()).toBeInTheDocument();
|
|
297
|
+
// ===================
|
|
298
|
+
|
|
299
|
+
expect(screen.getByRole('combobox', { name: /Select a location/i })).toBeInTheDocument();
|
|
300
|
+
expect(screen.getByRole('radio', { name: /HIV Return Visit/ })).toBeInTheDocument();
|
|
301
|
+
expect(screen.getByText(/Punctuality/i)).toBeInTheDocument();
|
|
302
|
+
|
|
303
|
+
expect(screen.getByRole('button', { name: /Start Visit/i })).toBeInTheDocument();
|
|
304
|
+
expect(screen.getByRole('button', { name: /Discard/i })).toBeInTheDocument();
|
|
305
|
+
|
|
306
|
+
// Testing the location picker
|
|
307
|
+
const combobox = screen.getByRole('combobox', { name: /Select a location/i });
|
|
308
|
+
expect(screen.getByText(/Outpatient Visit/i)).toBeInTheDocument();
|
|
309
|
+
await user.click(combobox);
|
|
310
|
+
expect(screen.getByText(/Mosoriot/i)).toBeInTheDocument();
|
|
311
|
+
expect(screen.getByText(/Inpatient Ward/i)).toBeInTheDocument();
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
it('does not render visit type combo box if atFacilityVisitType set', async () => {
|
|
315
|
+
mockUseEmrConfiguration.mockReturnValue({
|
|
316
|
+
emrConfiguration: {
|
|
317
|
+
atFacilityVisitType: {
|
|
318
|
+
uuid: 'some-uuid1',
|
|
319
|
+
},
|
|
320
|
+
},
|
|
321
|
+
isLoadingEmrConfiguration: false,
|
|
322
|
+
errorFetchingEmrConfiguration: null,
|
|
323
|
+
mutateEmrConfiguration: null,
|
|
324
|
+
});
|
|
325
|
+
renderVisitForm();
|
|
326
|
+
expect(screen.queryByRole('radio', { name: /HIV Return Visit/ })).not.toBeInTheDocument();
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
it('renders a validation error when required fields are not filled', async () => {
|
|
330
|
+
const user = userEvent.setup();
|
|
331
|
+
|
|
332
|
+
renderVisitForm();
|
|
333
|
+
|
|
334
|
+
const saveButton = screen.getByRole('button', { name: /start visit/i });
|
|
335
|
+
const locationPicker = screen.getByRole('combobox', { name: /select a location/i });
|
|
336
|
+
await user.click(locationPicker);
|
|
337
|
+
await user.click(screen.getByText('Inpatient Ward'));
|
|
338
|
+
await user.click(saveButton);
|
|
339
|
+
|
|
340
|
+
expect(screen.getByText(/missing visit type/i)).toBeInTheDocument();
|
|
341
|
+
expect(screen.getByText(/please select a visit type/i)).toBeInTheDocument();
|
|
342
|
+
|
|
343
|
+
await user.click(screen.getByLabelText(/Outpatient visit/i));
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
it('keeps user selections when default values update', async () => {
|
|
347
|
+
const user = userEvent.setup();
|
|
348
|
+
mockUseDefaultVisitLocation.mockReturnValueOnce({}).mockReturnValue({
|
|
349
|
+
display: 'Inpatient Ward',
|
|
350
|
+
uuid: 'location-b',
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
const props: PatientWorkspace2DefinitionProps<VisitFormProps, {}> = {
|
|
354
|
+
...defaultProps,
|
|
355
|
+
groupProps: {
|
|
356
|
+
...defaultProps.groupProps,
|
|
357
|
+
visitContext: null,
|
|
358
|
+
},
|
|
359
|
+
};
|
|
360
|
+
const { rerender } = render(<VisitForm {...props} />);
|
|
361
|
+
|
|
362
|
+
const visitStatusOngoing = screen.getByRole('tab', { name: /ongoing/i });
|
|
363
|
+
await user.click(visitStatusOngoing);
|
|
364
|
+
const outpatientVisitRadio = screen.getByRole('radio', { name: /^Outpatient Visit$/i });
|
|
365
|
+
await user.click(outpatientVisitRadio);
|
|
366
|
+
|
|
367
|
+
expect(outpatientVisitRadio).toBeChecked();
|
|
368
|
+
|
|
369
|
+
rerender(<VisitForm {...props} />);
|
|
370
|
+
|
|
371
|
+
await waitFor(() => expect(screen.getByRole('radio', { name: /^Outpatient Visit$/i })).toBeChecked());
|
|
372
|
+
});
|
|
373
|
+
|
|
374
|
+
it('displays an error message when the visit start time is in the future', async () => {
|
|
375
|
+
const user = userEvent.setup();
|
|
376
|
+
|
|
377
|
+
renderVisitForm();
|
|
378
|
+
|
|
379
|
+
await user.click(screen.getByRole('tab', { name: /ongoing/i }));
|
|
380
|
+
const dateInput = screen.getByRole('textbox', { name: /start date/i });
|
|
381
|
+
const timeInput = screen.getByRole('textbox', { name: /start time/i });
|
|
382
|
+
const amPmSelect = screen.getByRole('combobox', { name: /start time format/i });
|
|
383
|
+
const saveButton = screen.getByRole('button', { name: /Start visit/i });
|
|
384
|
+
const futureTime = dayjs().add(1, 'hour');
|
|
385
|
+
|
|
386
|
+
expect(dateInput).toBeEnabled();
|
|
387
|
+
await user.clear(dateInput);
|
|
388
|
+
await user.click(dateInput);
|
|
389
|
+
fireEvent.change(dateInput, { target: { value: futureTime.format('YYYY-MM-DD') } });
|
|
390
|
+
|
|
391
|
+
await user.clear(timeInput);
|
|
392
|
+
await user.type(timeInput, futureTime.format('hh:mm'));
|
|
393
|
+
await user.selectOptions(amPmSelect, futureTime.format('A'));
|
|
394
|
+
await user.tab();
|
|
395
|
+
|
|
396
|
+
// ***
|
|
397
|
+
// For some reason, DatePicker and TimePicker does not get react hook form's zod validation
|
|
398
|
+
// errors with onChange events when in unit tests. Validation triggers only when
|
|
399
|
+
// the required fields (Visit type and location) are filled out and the save button is pressed.
|
|
400
|
+
|
|
401
|
+
// Set Visit type
|
|
402
|
+
await user.click(screen.getByLabelText(/Outpatient visit/i));
|
|
403
|
+
// Set location
|
|
404
|
+
const locationPicker = screen.getByRole('combobox', { name: /Select a location/i });
|
|
405
|
+
await user.click(locationPicker);
|
|
406
|
+
await user.click(screen.getByText('Inpatient Ward'));
|
|
407
|
+
await user.click(saveButton);
|
|
408
|
+
|
|
409
|
+
expect(timeInput).toHaveValue(futureTime.format('hh:mm'));
|
|
410
|
+
expect(screen.getByText(/start time cannot be in the future/i)).toBeInTheDocument();
|
|
411
|
+
});
|
|
412
|
+
|
|
413
|
+
// FIXME: Make the date input work
|
|
414
|
+
it.skip('allows to enter start date in the past when visit status is ongoing', async () => {
|
|
415
|
+
const user = userEvent.setup();
|
|
416
|
+
|
|
417
|
+
renderVisitForm();
|
|
418
|
+
|
|
419
|
+
await user.click(screen.getByRole('tab', { name: /ongoing/i }));
|
|
420
|
+
const dateInput = screen.queryByLabelText(/start date/i) as HTMLInputElement;
|
|
421
|
+
const timeInput = screen.getByRole('textbox', { name: /start time/i }) as HTMLInputElement;
|
|
422
|
+
const amPmSelect = screen.getByRole('combobox', { name: /start time format/i });
|
|
423
|
+
const pastTime = dayjs().subtract(1, 'month');
|
|
424
|
+
|
|
425
|
+
expect(dateInput).toBeEnabled();
|
|
426
|
+
await user.clear(dateInput);
|
|
427
|
+
await user.click(dateInput);
|
|
428
|
+
await user.type(dateInput, pastTime.format('DD/MM/YYYY') + '{enter}');
|
|
429
|
+
await user.tab();
|
|
430
|
+
await user.type(timeInput, pastTime.format('hh:mm'));
|
|
431
|
+
await user.selectOptions(amPmSelect, pastTime.format('A'));
|
|
432
|
+
await user.tab();
|
|
433
|
+
|
|
434
|
+
expect(dateInput).toHaveValue(pastTime.format('DD/MM/YYYY'));
|
|
435
|
+
});
|
|
436
|
+
|
|
437
|
+
it('create a new visit upon successful submission of the form', async () => {
|
|
438
|
+
const user = userEvent.setup();
|
|
439
|
+
|
|
440
|
+
renderVisitForm();
|
|
441
|
+
|
|
442
|
+
const saveButton = screen.getByRole('button', { name: /Start visit/i });
|
|
443
|
+
|
|
444
|
+
// Set visit type
|
|
445
|
+
await user.click(screen.getByLabelText(/Outpatient visit/i));
|
|
446
|
+
|
|
447
|
+
// Set location
|
|
448
|
+
const locationPicker = screen.getByRole('combobox', { name: /Select a location/i });
|
|
449
|
+
await user.click(locationPicker);
|
|
450
|
+
await user.click(screen.getByText('Inpatient Ward'));
|
|
451
|
+
|
|
452
|
+
await user.click(saveButton);
|
|
453
|
+
expect(mockSaveVisit).toHaveBeenCalledTimes(1);
|
|
454
|
+
expect(mockSaveVisit).toHaveBeenCalledWith(
|
|
455
|
+
{
|
|
456
|
+
location: mockLocations[1].uuid,
|
|
457
|
+
patient: mockPatient.id,
|
|
458
|
+
visitType: 'some-uuid1',
|
|
459
|
+
startDatetime: null,
|
|
460
|
+
stopDatetime: null,
|
|
461
|
+
},
|
|
462
|
+
expect.any(Object),
|
|
463
|
+
);
|
|
464
|
+
|
|
465
|
+
expect(showSnackbar).toHaveBeenCalledTimes(1);
|
|
466
|
+
expect(showSnackbar).toHaveBeenCalledWith({
|
|
467
|
+
isLowContrast: true,
|
|
468
|
+
subtitle: expect.stringContaining('started successfully'),
|
|
469
|
+
kind: 'success',
|
|
470
|
+
title: 'Visit started',
|
|
471
|
+
});
|
|
472
|
+
});
|
|
473
|
+
|
|
474
|
+
it('reports no unsaved changes after a successful save, even while callbacks are still pending', async () => {
|
|
475
|
+
const user = userEvent.setup();
|
|
476
|
+
|
|
477
|
+
let resolveCallback: () => void;
|
|
478
|
+
const pendingCallback = new Promise<void>((resolve) => {
|
|
479
|
+
resolveCallback = resolve;
|
|
480
|
+
});
|
|
481
|
+
mockOnVisitCreatedOrUpdatedCallback.mockReturnValueOnce(pendingCallback);
|
|
482
|
+
|
|
483
|
+
renderVisitForm();
|
|
484
|
+
|
|
485
|
+
await user.click(screen.getByLabelText(/Outpatient visit/i));
|
|
486
|
+
const locationPicker = screen.getByRole('combobox', { name: /Select a location/i });
|
|
487
|
+
await user.click(locationPicker);
|
|
488
|
+
await user.click(screen.getByText('Inpatient Ward'));
|
|
489
|
+
|
|
490
|
+
await user.click(screen.getByRole('button', { name: /Start visit/i }));
|
|
491
|
+
|
|
492
|
+
await waitFor(() => expect(mockSaveVisit).toHaveBeenCalledTimes(1));
|
|
493
|
+
|
|
494
|
+
await waitFor(() => {
|
|
495
|
+
const lastCall = mockWorkspace2.mock.lastCall?.[0];
|
|
496
|
+
expect(lastCall?.hasUnsavedChanges).toBe(false);
|
|
497
|
+
});
|
|
498
|
+
|
|
499
|
+
resolveCallback();
|
|
500
|
+
});
|
|
501
|
+
|
|
502
|
+
it('starts a new visit with attributes upon successful submission of the form', async () => {
|
|
503
|
+
const user = userEvent.setup();
|
|
504
|
+
|
|
505
|
+
renderVisitForm();
|
|
506
|
+
|
|
507
|
+
const saveButton = screen.getByRole('button', { name: /Start visit/i });
|
|
508
|
+
|
|
509
|
+
// Set visit type
|
|
510
|
+
await user.click(screen.getByLabelText(/Outpatient visit/i));
|
|
511
|
+
|
|
512
|
+
// Set location
|
|
513
|
+
const locationPicker = screen.getByRole('combobox', { name: /Select a location/i });
|
|
514
|
+
await user.click(locationPicker);
|
|
515
|
+
await user.click(screen.getByText('Inpatient Ward'));
|
|
516
|
+
|
|
517
|
+
const punctualityPicker = screen.getByRole('combobox', { name: 'Punctuality (optional)' });
|
|
518
|
+
await user.selectOptions(punctualityPicker, 'On time');
|
|
519
|
+
|
|
520
|
+
const insuranceNumberInput = screen.getByRole('textbox', { name: 'Insurance Policy Number (optional)' });
|
|
521
|
+
await user.clear(insuranceNumberInput);
|
|
522
|
+
await user.type(insuranceNumberInput, '183299');
|
|
523
|
+
|
|
524
|
+
await user.click(saveButton);
|
|
525
|
+
|
|
526
|
+
expect(mockSaveVisit).toHaveBeenCalledTimes(1);
|
|
527
|
+
expect(mockSaveVisit).toHaveBeenCalledWith(
|
|
528
|
+
{
|
|
529
|
+
attributes: [
|
|
530
|
+
{
|
|
531
|
+
attributeType: visitAttributes.punctuality.uuid,
|
|
532
|
+
value: '66cdc0a1-aa19-4676-af51-80f66d78d9eb',
|
|
533
|
+
},
|
|
534
|
+
{
|
|
535
|
+
attributeType: visitAttributes.insurancePolicyNumber.uuid,
|
|
536
|
+
value: '183299',
|
|
537
|
+
},
|
|
538
|
+
],
|
|
539
|
+
location: mockLocations[1].uuid,
|
|
540
|
+
patient: mockPatient.id,
|
|
541
|
+
visitType: 'some-uuid1',
|
|
542
|
+
startDatetime: null,
|
|
543
|
+
stopDatetime: null,
|
|
544
|
+
},
|
|
545
|
+
expect.any(Object),
|
|
546
|
+
);
|
|
547
|
+
|
|
548
|
+
// Attributes should be included in the visit payload, not created separately
|
|
549
|
+
expect(mockCreateVisitAttribute).not.toHaveBeenCalled();
|
|
550
|
+
|
|
551
|
+
expect(mockOnVisitCreatedOrUpdatedCallback).toHaveBeenCalled();
|
|
552
|
+
|
|
553
|
+
expect(mockCloseWorkspace).toHaveBeenCalled();
|
|
554
|
+
|
|
555
|
+
expect(showSnackbar).toHaveBeenCalledTimes(1);
|
|
556
|
+
expect(showSnackbar).toHaveBeenCalledWith({
|
|
557
|
+
isLowContrast: true,
|
|
558
|
+
subtitle: expect.stringContaining('started successfully'),
|
|
559
|
+
kind: 'success',
|
|
560
|
+
title: 'Visit started',
|
|
561
|
+
});
|
|
562
|
+
});
|
|
563
|
+
|
|
564
|
+
it('updates visit attributes when editing an existing visit', async () => {
|
|
565
|
+
const user = userEvent.setup();
|
|
566
|
+
|
|
567
|
+
renderVisitForm(mockVisitWithAttributes);
|
|
568
|
+
|
|
569
|
+
const saveButton = screen.getByRole('button', { name: /Update visit/i });
|
|
570
|
+
|
|
571
|
+
// Set visit type
|
|
572
|
+
await user.click(screen.getByLabelText(/Outpatient visit/i));
|
|
573
|
+
|
|
574
|
+
// Set location
|
|
575
|
+
const locationPicker = screen.getByRole('combobox', { name: /Select a location/i });
|
|
576
|
+
await user.click(locationPicker);
|
|
577
|
+
await user.click(screen.getByText('Inpatient Ward'));
|
|
578
|
+
|
|
579
|
+
const punctualityPicker = screen.getByRole('combobox', { name: 'Punctuality (optional)' });
|
|
580
|
+
await user.selectOptions(punctualityPicker, 'Late');
|
|
581
|
+
|
|
582
|
+
const insuranceNumberInput = screen.getByRole('textbox', { name: 'Insurance Policy Number (optional)' });
|
|
583
|
+
await user.clear(insuranceNumberInput);
|
|
584
|
+
await user.type(insuranceNumberInput, '1873290');
|
|
585
|
+
|
|
586
|
+
mockUpdateVisit.mockResolvedValue({
|
|
587
|
+
status: 201,
|
|
588
|
+
data: {
|
|
589
|
+
uuid: visitUuid,
|
|
590
|
+
visitType: {
|
|
591
|
+
display: 'Facility Visit',
|
|
592
|
+
},
|
|
593
|
+
},
|
|
594
|
+
} as unknown as FetchResponse<Visit>);
|
|
595
|
+
|
|
596
|
+
await user.click(saveButton);
|
|
597
|
+
|
|
598
|
+
expect(mockUpdateVisit).toHaveBeenCalledWith(
|
|
599
|
+
mockVisitWithAttributes.uuid,
|
|
600
|
+
expect.objectContaining({
|
|
601
|
+
location: mockLocations[1].uuid,
|
|
602
|
+
visitType: 'some-uuid1',
|
|
603
|
+
}),
|
|
604
|
+
expect.any(Object),
|
|
605
|
+
);
|
|
606
|
+
// Inline attributes must not be included in the update payload because the
|
|
607
|
+
// backend rejects them with a maxOccurs violation. Attributes are managed
|
|
608
|
+
// separately via individual create/update/delete calls for existing visits.
|
|
609
|
+
expect(mockUpdateVisit.mock.calls[0][1]).not.toHaveProperty('attributes');
|
|
610
|
+
|
|
611
|
+
expect(mockUpdateVisitAttribute).toHaveBeenCalledTimes(2);
|
|
612
|
+
expect(mockUpdateVisitAttribute).toHaveBeenCalledWith(
|
|
613
|
+
visitUuid,
|
|
614
|
+
'c98e66d7-7db5-47ae-b46f-91a0f3b6dda1',
|
|
615
|
+
'66cdc0a1-aa19-4676-af51-80f66d78d9ec',
|
|
616
|
+
);
|
|
617
|
+
expect(mockUpdateVisitAttribute).toHaveBeenCalledWith(visitUuid, 'd6d7d26a-5975-4f03-8abb-db073c948897', '1873290');
|
|
618
|
+
|
|
619
|
+
expect(mockCloseWorkspace).toHaveBeenCalled();
|
|
620
|
+
expect(showSnackbar).toHaveBeenCalledWith({
|
|
621
|
+
isLowContrast: true,
|
|
622
|
+
subtitle: 'Facility Visit updated successfully',
|
|
623
|
+
kind: 'success',
|
|
624
|
+
title: 'Visit details updated',
|
|
625
|
+
});
|
|
626
|
+
});
|
|
627
|
+
|
|
628
|
+
it('deletes visit attributes if the value of the field is cleared when editing an existing visit', async () => {
|
|
629
|
+
const user = userEvent.setup();
|
|
630
|
+
|
|
631
|
+
renderVisitForm(mockVisitWithAttributes);
|
|
632
|
+
|
|
633
|
+
const saveButton = screen.getByRole('button', { name: /Update visit/i });
|
|
634
|
+
|
|
635
|
+
// Set visit type
|
|
636
|
+
await user.click(screen.getByLabelText(/Outpatient visit/i));
|
|
637
|
+
|
|
638
|
+
// Set location
|
|
639
|
+
const locationPicker = screen.getByRole('combobox', { name: /Select a location/i });
|
|
640
|
+
await user.click(locationPicker);
|
|
641
|
+
await user.click(screen.getByText('Inpatient Ward'));
|
|
642
|
+
|
|
643
|
+
const punctualityPicker = screen.getByRole('combobox', { name: 'Punctuality (optional)' });
|
|
644
|
+
await user.selectOptions(punctualityPicker, 'Select an option');
|
|
645
|
+
|
|
646
|
+
const insuranceNumberInput = screen.getByRole('textbox', { name: 'Insurance Policy Number (optional)' });
|
|
647
|
+
await user.clear(insuranceNumberInput);
|
|
648
|
+
|
|
649
|
+
mockUpdateVisit.mockResolvedValue({
|
|
650
|
+
status: 201,
|
|
651
|
+
data: {
|
|
652
|
+
uuid: visitUuid,
|
|
653
|
+
visitType: {
|
|
654
|
+
display: 'Facility Visit',
|
|
655
|
+
},
|
|
656
|
+
},
|
|
657
|
+
} as unknown as FetchResponse<Visit>);
|
|
658
|
+
|
|
659
|
+
await user.click(saveButton);
|
|
660
|
+
|
|
661
|
+
expect(mockUpdateVisit).toHaveBeenCalledWith(
|
|
662
|
+
mockVisitWithAttributes.uuid,
|
|
663
|
+
expect.objectContaining({
|
|
664
|
+
location: mockLocations[1].uuid,
|
|
665
|
+
visitType: 'some-uuid1',
|
|
666
|
+
}),
|
|
667
|
+
expect.any(Object),
|
|
668
|
+
);
|
|
669
|
+
|
|
670
|
+
expect(mockDeleteVisitAttribute).toHaveBeenCalledTimes(2);
|
|
671
|
+
expect(mockDeleteVisitAttribute).toHaveBeenCalledWith(visitUuid, 'c98e66d7-7db5-47ae-b46f-91a0f3b6dda1');
|
|
672
|
+
expect(mockDeleteVisitAttribute).toHaveBeenCalledWith(visitUuid, 'd6d7d26a-5975-4f03-8abb-db073c948897');
|
|
673
|
+
|
|
674
|
+
expect(mockCloseWorkspace).toHaveBeenCalled();
|
|
675
|
+
|
|
676
|
+
expect(showSnackbar).toHaveBeenCalledWith({
|
|
677
|
+
isLowContrast: true,
|
|
678
|
+
subtitle: 'Facility Visit updated successfully',
|
|
679
|
+
kind: 'success',
|
|
680
|
+
title: 'Visit details updated',
|
|
681
|
+
});
|
|
682
|
+
});
|
|
683
|
+
|
|
684
|
+
it('renders an error message if there was a problem starting a new visit', async () => {
|
|
685
|
+
const user = userEvent.setup();
|
|
686
|
+
|
|
687
|
+
mockSaveVisit.mockRejectedValueOnce({ status: 500, statusText: 'Internal server error' });
|
|
688
|
+
|
|
689
|
+
renderVisitForm();
|
|
690
|
+
|
|
691
|
+
await user.click(screen.getByLabelText(/Outpatient visit/i));
|
|
692
|
+
|
|
693
|
+
const saveButton = screen.getByRole('button', { name: /Start Visit/i });
|
|
694
|
+
const locationPicker = screen.getByRole('combobox', { name: /Select a location/i });
|
|
695
|
+
await user.click(locationPicker);
|
|
696
|
+
await user.click(screen.getByText(/Inpatient Ward/i));
|
|
697
|
+
|
|
698
|
+
await user.click(saveButton);
|
|
699
|
+
|
|
700
|
+
expect(showSnackbar).toHaveBeenCalledTimes(1);
|
|
701
|
+
expect(showSnackbar).toHaveBeenCalledWith(
|
|
702
|
+
expect.objectContaining({
|
|
703
|
+
kind: 'error',
|
|
704
|
+
title: 'Error starting visit',
|
|
705
|
+
}),
|
|
706
|
+
);
|
|
707
|
+
|
|
708
|
+
expect(mockOnVisitCreatedOrUpdatedCallback).not.toHaveBeenCalled();
|
|
709
|
+
expect(mockCloseWorkspace).not.toHaveBeenCalled();
|
|
710
|
+
});
|
|
711
|
+
|
|
712
|
+
it('does not create visit attributes separately when starting a new visit with attributes', async () => {
|
|
713
|
+
const user = userEvent.setup();
|
|
714
|
+
|
|
715
|
+
renderVisitForm();
|
|
716
|
+
|
|
717
|
+
await user.click(screen.getByLabelText(/Outpatient visit/i));
|
|
718
|
+
|
|
719
|
+
const saveButton = screen.getByRole('button', { name: /Start Visit/i });
|
|
720
|
+
const locationPicker = screen.getByRole('combobox', { name: /Select a location/i });
|
|
721
|
+
await user.click(locationPicker);
|
|
722
|
+
await user.click(screen.getByText(/Inpatient Ward/i));
|
|
723
|
+
|
|
724
|
+
const punctualityPicker = screen.getByRole('combobox', { name: 'Punctuality (optional)' });
|
|
725
|
+
await user.selectOptions(punctualityPicker, 'On time');
|
|
726
|
+
|
|
727
|
+
const insuranceNumberInput = screen.getByRole('textbox', { name: 'Insurance Policy Number (optional)' });
|
|
728
|
+
await user.clear(insuranceNumberInput);
|
|
729
|
+
await user.type(insuranceNumberInput, '183299');
|
|
730
|
+
|
|
731
|
+
await user.click(saveButton);
|
|
732
|
+
|
|
733
|
+
// Attributes should be included in the saveVisit payload, not created via separate API calls
|
|
734
|
+
expect(mockSaveVisit).toHaveBeenCalledWith(
|
|
735
|
+
expect.objectContaining({
|
|
736
|
+
attributes: expect.arrayContaining([
|
|
737
|
+
{ attributeType: visitAttributes.punctuality.uuid, value: '66cdc0a1-aa19-4676-af51-80f66d78d9eb' },
|
|
738
|
+
{ attributeType: visitAttributes.insurancePolicyNumber.uuid, value: '183299' },
|
|
739
|
+
]),
|
|
740
|
+
}),
|
|
741
|
+
expect.any(Object),
|
|
742
|
+
);
|
|
743
|
+
expect(mockCreateVisitAttribute).not.toHaveBeenCalled();
|
|
744
|
+
});
|
|
745
|
+
|
|
746
|
+
it('does not create an orphaned visit when the server rejects a new visit with attributes', async () => {
|
|
747
|
+
const user = userEvent.setup();
|
|
748
|
+
|
|
749
|
+
mockSaveVisit.mockRejectedValueOnce({ status: 400, statusText: 'Bad Request' });
|
|
750
|
+
|
|
751
|
+
renderVisitForm();
|
|
752
|
+
|
|
753
|
+
await user.click(screen.getByLabelText(/Outpatient visit/i));
|
|
754
|
+
|
|
755
|
+
const saveButton = screen.getByRole('button', { name: /Start Visit/i });
|
|
756
|
+
const locationPicker = screen.getByRole('combobox', { name: /Select a location/i });
|
|
757
|
+
await user.click(locationPicker);
|
|
758
|
+
await user.click(screen.getByText(/Inpatient Ward/i));
|
|
759
|
+
|
|
760
|
+
const punctualityPicker = screen.getByRole('combobox', { name: 'Punctuality (optional)' });
|
|
761
|
+
await user.selectOptions(punctualityPicker, 'On time');
|
|
762
|
+
|
|
763
|
+
const insuranceNumberInput = screen.getByRole('textbox', { name: 'Insurance Policy Number (optional)' });
|
|
764
|
+
await user.clear(insuranceNumberInput);
|
|
765
|
+
await user.type(insuranceNumberInput, '183299');
|
|
766
|
+
|
|
767
|
+
await user.click(saveButton);
|
|
768
|
+
|
|
769
|
+
// Attributes are included in the saveVisit payload, so when the visit creation
|
|
770
|
+
// is rejected (e.g. due to overlapping visits), no orphaned visit is created
|
|
771
|
+
// and no separate attribute creation calls are made
|
|
772
|
+
expect(mockSaveVisit).toHaveBeenCalledWith(
|
|
773
|
+
expect.objectContaining({
|
|
774
|
+
attributes: expect.arrayContaining([
|
|
775
|
+
{ attributeType: visitAttributes.punctuality.uuid, value: '66cdc0a1-aa19-4676-af51-80f66d78d9eb' },
|
|
776
|
+
{ attributeType: visitAttributes.insurancePolicyNumber.uuid, value: '183299' },
|
|
777
|
+
]),
|
|
778
|
+
}),
|
|
779
|
+
expect.any(Object),
|
|
780
|
+
);
|
|
781
|
+
expect(mockCreateVisitAttribute).not.toHaveBeenCalled();
|
|
782
|
+
expect(mockOnVisitCreatedOrUpdatedCallback).not.toHaveBeenCalled();
|
|
783
|
+
expect(mockCloseWorkspace).not.toHaveBeenCalled();
|
|
784
|
+
});
|
|
785
|
+
|
|
786
|
+
it('displays a warning modal if the user attempts to discard the visit form with unsaved changes', async () => {
|
|
787
|
+
const user = userEvent.setup();
|
|
788
|
+
|
|
789
|
+
renderVisitForm();
|
|
790
|
+
|
|
791
|
+
await user.click(screen.getByLabelText(/Outpatient visit/i));
|
|
792
|
+
|
|
793
|
+
const closeButton = screen.getByRole('button', { name: /Discard/i });
|
|
794
|
+
|
|
795
|
+
await user.click(closeButton);
|
|
796
|
+
|
|
797
|
+
expect(mockCloseWorkspace).toHaveBeenCalled();
|
|
798
|
+
});
|
|
799
|
+
|
|
800
|
+
it('renders an inline error notification if an optional visit attribute type field fails to load', async () => {
|
|
801
|
+
mockUseVisitAttributeType.mockReturnValue({
|
|
802
|
+
isLoading: false,
|
|
803
|
+
error: new Error('failed to load'),
|
|
804
|
+
data: visitAttributes.punctuality,
|
|
805
|
+
});
|
|
806
|
+
|
|
807
|
+
renderVisitForm();
|
|
808
|
+
|
|
809
|
+
expect(screen.getByText(/Part of the form did not load/i)).toBeInTheDocument();
|
|
810
|
+
expect(screen.getByText(/Please refresh to try again/i)).toBeInTheDocument();
|
|
811
|
+
expect(screen.getByRole('button', { name: /Start visit/i })).toBeEnabled();
|
|
812
|
+
});
|
|
813
|
+
|
|
814
|
+
it('renders an error if a required visit attribute type is not provided', async () => {
|
|
815
|
+
const user = userEvent.setup();
|
|
816
|
+
|
|
817
|
+
mockUseConfig.mockReturnValue({
|
|
818
|
+
...(getDefaultsFromConfigSchema(esmPatientChartSchema) as ChartConfig),
|
|
819
|
+
visitAttributeTypes: [
|
|
820
|
+
{
|
|
821
|
+
uuid: visitAttributes.punctuality.uuid,
|
|
822
|
+
required: true,
|
|
823
|
+
displayInThePatientBanner: true,
|
|
824
|
+
},
|
|
825
|
+
],
|
|
826
|
+
});
|
|
827
|
+
|
|
828
|
+
renderVisitForm();
|
|
829
|
+
|
|
830
|
+
const saveButton = screen.getByRole('button', { name: /Start visit/i });
|
|
831
|
+
|
|
832
|
+
// Set visit type
|
|
833
|
+
await user.click(screen.getByLabelText(/Outpatient visit/i));
|
|
834
|
+
|
|
835
|
+
// Set location
|
|
836
|
+
const locationPicker = screen.getByRole('combobox', { name: /Select a location/i });
|
|
837
|
+
await user.click(locationPicker);
|
|
838
|
+
await user.click(screen.getByText('Inpatient Ward'));
|
|
839
|
+
await user.click(saveButton);
|
|
840
|
+
|
|
841
|
+
expect(mockSaveVisit).not.toHaveBeenCalled();
|
|
842
|
+
});
|
|
843
|
+
|
|
844
|
+
it('should disable the submit button and display an inline error notification if required visit attribute fields fail to load', async () => {
|
|
845
|
+
mockUseVisitAttributeType.mockReturnValue({
|
|
846
|
+
isLoading: false,
|
|
847
|
+
error: new Error('failed to load'),
|
|
848
|
+
data: visitAttributes.punctuality,
|
|
849
|
+
});
|
|
850
|
+
|
|
851
|
+
mockUseConfig.mockReturnValue({
|
|
852
|
+
...getDefaultsFromConfigSchema(esmPatientChartSchema),
|
|
853
|
+
visitAttributeTypes: [
|
|
854
|
+
{
|
|
855
|
+
uuid: visitAttributes.punctuality.uuid,
|
|
856
|
+
required: true,
|
|
857
|
+
displayInThePatientBanner: true,
|
|
858
|
+
},
|
|
859
|
+
],
|
|
860
|
+
} as ChartConfig);
|
|
861
|
+
|
|
862
|
+
renderVisitForm();
|
|
863
|
+
|
|
864
|
+
expect(screen.getByText(/Part of the form did not load/i)).toBeInTheDocument();
|
|
865
|
+
expect(screen.getByText(/Please refresh to try again/i)).toBeInTheDocument();
|
|
866
|
+
expect(screen.getByRole('button', { name: /Start visit/i })).toBeDisabled();
|
|
867
|
+
});
|
|
868
|
+
|
|
869
|
+
it('shows an active visit warning, hides form fields, and disables submit when overlapping visits are disallowed and an active visit exists', () => {
|
|
870
|
+
mockUseVisit.mockReturnValue({
|
|
871
|
+
activeVisit: { uuid: 'some-active-visit-uuid' } as Visit,
|
|
872
|
+
} as ReturnType<typeof useVisit>);
|
|
873
|
+
mockUseAllowOverlappingVisits.mockReturnValue({ allowOverlappingVisits: false, isLoading: false });
|
|
874
|
+
|
|
875
|
+
renderVisitForm();
|
|
876
|
+
|
|
877
|
+
expect(screen.getByText(/This patient already has an active visit/i)).toBeInTheDocument();
|
|
878
|
+
expect(screen.getByText(/You must end the current visit before starting a new one/i)).toBeInTheDocument();
|
|
879
|
+
expect(screen.getByRole('button', { name: /Start visit/i })).toBeDisabled();
|
|
880
|
+
|
|
881
|
+
// Form fields should be hidden
|
|
882
|
+
expect(screen.queryByRole('combobox', { name: /Select a location/i })).not.toBeInTheDocument();
|
|
883
|
+
expect(screen.queryByRole('radio', { name: /Outpatient Visit/i })).not.toBeInTheDocument();
|
|
884
|
+
});
|
|
885
|
+
|
|
886
|
+
it('does not show an active visit warning when overlapping visits are allowed', () => {
|
|
887
|
+
mockUseVisit.mockReturnValue({
|
|
888
|
+
activeVisit: { uuid: 'some-active-visit-uuid' } as Visit,
|
|
889
|
+
} as ReturnType<typeof useVisit>);
|
|
890
|
+
mockUseAllowOverlappingVisits.mockReturnValue({ allowOverlappingVisits: true, isLoading: false });
|
|
891
|
+
|
|
892
|
+
renderVisitForm();
|
|
893
|
+
|
|
894
|
+
expect(screen.queryByText(/This patient already has an active visit/i)).not.toBeInTheDocument();
|
|
895
|
+
expect(screen.getByRole('button', { name: /Start visit/i })).toBeEnabled();
|
|
896
|
+
});
|
|
897
|
+
|
|
898
|
+
it('shows active visit warning on the ongoing tab when overlapping visits are disallowed', async () => {
|
|
899
|
+
const user = userEvent.setup();
|
|
900
|
+
mockUseVisit.mockReturnValue({
|
|
901
|
+
activeVisit: { uuid: 'some-active-visit-uuid' } as Visit,
|
|
902
|
+
} as ReturnType<typeof useVisit>);
|
|
903
|
+
mockUseAllowOverlappingVisits.mockReturnValue({ allowOverlappingVisits: false, isLoading: false });
|
|
904
|
+
|
|
905
|
+
renderVisitForm();
|
|
906
|
+
|
|
907
|
+
await user.click(screen.getByRole('tab', { name: /ongoing/i }));
|
|
908
|
+
|
|
909
|
+
expect(screen.getByText(/This patient already has an active visit/i)).toBeInTheDocument();
|
|
910
|
+
expect(screen.getByRole('button', { name: /Start visit/i })).toBeDisabled();
|
|
911
|
+
});
|
|
912
|
+
|
|
913
|
+
it('does not show active visit warning on the past tab when overlapping visits are disallowed', async () => {
|
|
914
|
+
const user = userEvent.setup();
|
|
915
|
+
mockUseVisit.mockReturnValue({
|
|
916
|
+
activeVisit: { uuid: 'some-active-visit-uuid' } as Visit,
|
|
917
|
+
} as ReturnType<typeof useVisit>);
|
|
918
|
+
mockUseAllowOverlappingVisits.mockReturnValue({ allowOverlappingVisits: false, isLoading: false });
|
|
919
|
+
|
|
920
|
+
renderVisitForm();
|
|
921
|
+
|
|
922
|
+
await user.click(screen.getByRole('tab', { name: /in the past/i }));
|
|
923
|
+
|
|
924
|
+
expect(screen.queryByText(/This patient already has an active visit/i)).not.toBeInTheDocument();
|
|
925
|
+
expect(screen.getByRole('combobox', { name: /Select a location/i })).toBeInTheDocument();
|
|
926
|
+
});
|
|
927
|
+
|
|
928
|
+
it('disables submit while the active visit lookup is loading', () => {
|
|
929
|
+
mockUseVisit.mockReturnValue({
|
|
930
|
+
activeVisit: null,
|
|
931
|
+
isLoading: true,
|
|
932
|
+
} as ReturnType<typeof useVisit>);
|
|
933
|
+
mockUseAllowOverlappingVisits.mockReturnValue({ allowOverlappingVisits: false, isLoading: false });
|
|
934
|
+
|
|
935
|
+
renderVisitForm();
|
|
936
|
+
|
|
937
|
+
expect(screen.getByRole('button', { name: /Start visit/i })).toBeDisabled();
|
|
938
|
+
});
|
|
939
|
+
|
|
940
|
+
it('does not show an active visit warning when there is no active visit', () => {
|
|
941
|
+
mockUseVisit.mockReturnValue({
|
|
942
|
+
activeVisit: null,
|
|
943
|
+
} as ReturnType<typeof useVisit>);
|
|
944
|
+
mockUseAllowOverlappingVisits.mockReturnValue({ allowOverlappingVisits: false, isLoading: false });
|
|
945
|
+
|
|
946
|
+
renderVisitForm();
|
|
947
|
+
|
|
948
|
+
expect(screen.queryByText(/This patient already has an active visit/i)).not.toBeInTheDocument();
|
|
949
|
+
expect(screen.getByRole('button', { name: /Start visit/i })).toBeEnabled();
|
|
950
|
+
});
|
|
951
|
+
|
|
952
|
+
it('does not show an active visit warning when editing an existing visit', () => {
|
|
953
|
+
mockUseVisit.mockReturnValue({
|
|
954
|
+
activeVisit: { uuid: 'some-active-visit-uuid' } as Visit,
|
|
955
|
+
} as ReturnType<typeof useVisit>);
|
|
956
|
+
mockUseAllowOverlappingVisits.mockReturnValue({ allowOverlappingVisits: false, isLoading: false });
|
|
957
|
+
|
|
958
|
+
renderVisitForm(mockPastVisitWithEncounters);
|
|
959
|
+
|
|
960
|
+
expect(screen.queryByText(/This patient already has an active visit/i)).not.toBeInTheDocument();
|
|
961
|
+
expect(screen.getByRole('combobox', { name: /Select a location/i })).toBeInTheDocument();
|
|
962
|
+
});
|
|
963
|
+
});
|
|
964
|
+
|
|
965
|
+
// Note: For some reason, DatePicker and TimePicker don't get validated properly
|
|
966
|
+
// by react hook form + zod when in test. Here, we test validation of
|
|
967
|
+
// start / end time fields through the useVisitFormSchemaAndDefaultValues hook instead
|
|
968
|
+
describe('useVisitFormSchemaAndDefaultValues', () => {
|
|
969
|
+
beforeEach(() => {
|
|
970
|
+
mockUseConfig.mockReturnValue({
|
|
971
|
+
...getDefaultsFromConfigSchema(esmPatientChartSchema),
|
|
972
|
+
visitAttributeTypes: [
|
|
973
|
+
{
|
|
974
|
+
uuid: visitAttributes.punctuality.uuid,
|
|
975
|
+
required: false,
|
|
976
|
+
displayInThePatientBanner: true,
|
|
977
|
+
},
|
|
978
|
+
{
|
|
979
|
+
uuid: visitAttributes.insurancePolicyNumber.uuid,
|
|
980
|
+
required: false,
|
|
981
|
+
displayInThePatientBanner: true,
|
|
982
|
+
},
|
|
983
|
+
],
|
|
984
|
+
});
|
|
985
|
+
});
|
|
986
|
+
it('should validate start and end times', async () => {
|
|
987
|
+
const {
|
|
988
|
+
result: {
|
|
989
|
+
current: { visitFormSchema, defaultValues },
|
|
990
|
+
},
|
|
991
|
+
} = renderHook(() => useVisitFormSchemaAndDefaultValues(mockPastVisitWithEncounters));
|
|
992
|
+
|
|
993
|
+
// verify start time set to past end time
|
|
994
|
+
const stopDateTime = dayjs(mockPastVisitWithEncounters.stopDatetime);
|
|
995
|
+
const badStartTimePastStopTime = stopDateTime.add(1, 'hour');
|
|
996
|
+
const badStartTimePastStopTimeFields = convertToDateTimeFields(badStartTimePastStopTime);
|
|
997
|
+
const {
|
|
998
|
+
error: { issues: badStartTimePastStopTimeIssues },
|
|
999
|
+
} = visitFormSchema.safeParse({
|
|
1000
|
+
...defaultValues,
|
|
1001
|
+
visitStartDate: badStartTimePastStopTimeFields.date,
|
|
1002
|
+
visitStartTime: badStartTimePastStopTimeFields.time,
|
|
1003
|
+
visitStartTimeFormat: badStartTimePastStopTimeFields.timeFormat,
|
|
1004
|
+
});
|
|
1005
|
+
expect(badStartTimePastStopTimeIssues).toContainEqual(
|
|
1006
|
+
expect.objectContaining({
|
|
1007
|
+
message: 'End time must be after start time', //'Visit start time cannot be in the future'
|
|
1008
|
+
}),
|
|
1009
|
+
);
|
|
1010
|
+
|
|
1011
|
+
// verify start time set to future
|
|
1012
|
+
const badStartTimeInFuture = dayjs().add(1, 'hour');
|
|
1013
|
+
const badStartTimeInFutureFields = convertToDateTimeFields(badStartTimeInFuture);
|
|
1014
|
+
const {
|
|
1015
|
+
error: { issues: badStartTimeInFutureIssues },
|
|
1016
|
+
} = visitFormSchema.safeParse({
|
|
1017
|
+
...defaultValues,
|
|
1018
|
+
visitStartDate: badStartTimeInFutureFields.date,
|
|
1019
|
+
visitStartTime: badStartTimeInFutureFields.time,
|
|
1020
|
+
visitStartTimeFormat: badStartTimeInFutureFields.timeFormat,
|
|
1021
|
+
});
|
|
1022
|
+
expect(badStartTimeInFutureIssues).toContainEqual(
|
|
1023
|
+
expect.objectContaining({
|
|
1024
|
+
message: 'Start time cannot be in the future',
|
|
1025
|
+
}),
|
|
1026
|
+
);
|
|
1027
|
+
|
|
1028
|
+
// verify start time set past first encounter time
|
|
1029
|
+
const firstEncounterDatetime = dayjs(mockPastVisitWithEncounters.encounters[0].encounterDatetime);
|
|
1030
|
+
const badStartTimeAfterEncounterTime = firstEncounterDatetime.add(1, 'minute');
|
|
1031
|
+
const badStartTimeAfterEncounterTimeFields = convertToDateTimeFields(badStartTimeAfterEncounterTime);
|
|
1032
|
+
const {
|
|
1033
|
+
error: { issues: badStartTimeAfterEncounterTimeIssues },
|
|
1034
|
+
} = visitFormSchema.safeParse({
|
|
1035
|
+
...defaultValues,
|
|
1036
|
+
visitStartDate: badStartTimeAfterEncounterTimeFields.date,
|
|
1037
|
+
visitStartTime: badStartTimeAfterEncounterTimeFields.time,
|
|
1038
|
+
visitStartTimeFormat: badStartTimeAfterEncounterTimeFields.timeFormat,
|
|
1039
|
+
});
|
|
1040
|
+
expect(badStartTimeAfterEncounterTimeIssues).toContainEqual(
|
|
1041
|
+
expect.objectContaining({
|
|
1042
|
+
message: 'Start time must be on or before ' + firstEncounterDatetime.toDate().toLocaleString(),
|
|
1043
|
+
}),
|
|
1044
|
+
);
|
|
1045
|
+
|
|
1046
|
+
// verify stop time set to future
|
|
1047
|
+
const badStopTimeInFuture = dayjs().add(1, 'hour');
|
|
1048
|
+
const badStopTimeInFutureFields = convertToDateTimeFields(badStopTimeInFuture);
|
|
1049
|
+
const {
|
|
1050
|
+
error: { issues: badStopTimeInFutureIssues },
|
|
1051
|
+
} = visitFormSchema.safeParse({
|
|
1052
|
+
...defaultValues,
|
|
1053
|
+
visitStopDate: badStopTimeInFutureFields.date,
|
|
1054
|
+
visitStopTime: badStopTimeInFutureFields.time,
|
|
1055
|
+
visitStopTimeFormat: badStopTimeInFutureFields.timeFormat,
|
|
1056
|
+
});
|
|
1057
|
+
expect(badStopTimeInFutureIssues).toContainEqual(
|
|
1058
|
+
expect.objectContaining({
|
|
1059
|
+
message: 'End time cannot be in the future',
|
|
1060
|
+
}),
|
|
1061
|
+
);
|
|
1062
|
+
|
|
1063
|
+
// verify stop time set to before last encounter
|
|
1064
|
+
const lastEncounterDatetime = dayjs(mockPastVisitWithEncounters.encounters[1].encounterDatetime);
|
|
1065
|
+
const badStopTimeBeforeLastEncounter = lastEncounterDatetime.subtract(1, 'minute');
|
|
1066
|
+
const badStopTimeBeforeLastEncounterFields = convertToDateTimeFields(badStopTimeBeforeLastEncounter);
|
|
1067
|
+
const {
|
|
1068
|
+
error: { issues: badStopTimeBeforeLastEncounterIssues },
|
|
1069
|
+
} = visitFormSchema.safeParse({
|
|
1070
|
+
...defaultValues,
|
|
1071
|
+
visitStopDate: badStopTimeBeforeLastEncounterFields.date,
|
|
1072
|
+
visitStopTime: badStopTimeBeforeLastEncounterFields.time,
|
|
1073
|
+
visitStopTimeFormat: badStopTimeBeforeLastEncounterFields.timeFormat,
|
|
1074
|
+
});
|
|
1075
|
+
expect(badStopTimeBeforeLastEncounterIssues).toContainEqual(
|
|
1076
|
+
expect.objectContaining({
|
|
1077
|
+
message: 'End time must be on or after ' + lastEncounterDatetime.toDate().toLocaleString(),
|
|
1078
|
+
}),
|
|
1079
|
+
);
|
|
1080
|
+
});
|
|
1081
|
+
});
|
|
1082
|
+
|
|
1083
|
+
describe('computeEarliestAllowedStartDate', () => {
|
|
1084
|
+
it('returns null when birthdate is null', () => {
|
|
1085
|
+
expect(computeEarliestAllowedStartDate(null, false, null)).toBeNull();
|
|
1086
|
+
});
|
|
1087
|
+
|
|
1088
|
+
it('returns birthdate at local midnight for non-estimated birthdates', () => {
|
|
1089
|
+
const result = computeEarliestAllowedStartDate('1990-06-15', false, 35);
|
|
1090
|
+
expect(result).toEqual(new Date(1990, 5, 15));
|
|
1091
|
+
});
|
|
1092
|
+
|
|
1093
|
+
it('handles ISO datetime strings as returned by the REST API', () => {
|
|
1094
|
+
const result = computeEarliestAllowedStartDate('1979-12-08T00:00:00.000+0530', false, 46);
|
|
1095
|
+
expect(result).toEqual(new Date(1979, 11, 8));
|
|
1096
|
+
});
|
|
1097
|
+
|
|
1098
|
+
it('applies grace period for estimated birthdate with age 10 (shifts 5 years back)', () => {
|
|
1099
|
+
// floor(10 * 0.5) = 5, max(1, 5) = 5
|
|
1100
|
+
const result = computeEarliestAllowedStartDate('2015-03-01', true, 10);
|
|
1101
|
+
expect(result).toEqual(new Date(2010, 2, 1));
|
|
1102
|
+
});
|
|
1103
|
+
|
|
1104
|
+
it('applies minimum 1-year grace period for estimated birthdate with age 1', () => {
|
|
1105
|
+
// floor(1 * 0.5) = 0, max(1, 0) = 1
|
|
1106
|
+
const result = computeEarliestAllowedStartDate('2024-07-20', true, 1);
|
|
1107
|
+
expect(result).toEqual(new Date(2023, 6, 20));
|
|
1108
|
+
});
|
|
1109
|
+
|
|
1110
|
+
it('does not apply grace period when birthdateEstimated is false', () => {
|
|
1111
|
+
const result = computeEarliestAllowedStartDate('2000-01-01', false, 25);
|
|
1112
|
+
expect(result).toEqual(new Date(2000, 0, 1));
|
|
1113
|
+
});
|
|
1114
|
+
});
|
|
1115
|
+
|
|
1116
|
+
describe('useVisitFormSchemaAndDefaultValues birthdate validation', () => {
|
|
1117
|
+
beforeEach(() => {
|
|
1118
|
+
mockUseConfig.mockReturnValue({
|
|
1119
|
+
...getDefaultsFromConfigSchema(esmPatientChartSchema),
|
|
1120
|
+
visitAttributeTypes: [],
|
|
1121
|
+
});
|
|
1122
|
+
});
|
|
1123
|
+
|
|
1124
|
+
it('rejects a start date before the patient birthdate for ongoing visits', () => {
|
|
1125
|
+
const earliestAllowed = new Date(1990, 0, 1);
|
|
1126
|
+
const {
|
|
1127
|
+
result: {
|
|
1128
|
+
current: { visitFormSchema, defaultValues },
|
|
1129
|
+
},
|
|
1130
|
+
} = renderHook(() => useVisitFormSchemaAndDefaultValues(null, earliestAllowed));
|
|
1131
|
+
|
|
1132
|
+
const beforeBirthdate = dayjs(new Date(1989, 11, 31, 10, 0));
|
|
1133
|
+
const fields = convertToDateTimeFields(beforeBirthdate);
|
|
1134
|
+
|
|
1135
|
+
const { error } = visitFormSchema.safeParse({
|
|
1136
|
+
...defaultValues,
|
|
1137
|
+
visitStatus: 'ongoing',
|
|
1138
|
+
visitType: 'some-visit-type-uuid',
|
|
1139
|
+
visitStartDate: fields.date,
|
|
1140
|
+
visitStartTime: fields.time,
|
|
1141
|
+
visitStartTimeFormat: fields.timeFormat,
|
|
1142
|
+
});
|
|
1143
|
+
|
|
1144
|
+
expect(error.issues).toContainEqual(
|
|
1145
|
+
expect.objectContaining({
|
|
1146
|
+
message: "Start date cannot be before the patient's birth date",
|
|
1147
|
+
}),
|
|
1148
|
+
);
|
|
1149
|
+
});
|
|
1150
|
+
|
|
1151
|
+
it('accepts a start date equal to the patient birthdate', () => {
|
|
1152
|
+
const earliestAllowed = new Date(1990, 0, 1);
|
|
1153
|
+
const {
|
|
1154
|
+
result: {
|
|
1155
|
+
current: { visitFormSchema, defaultValues },
|
|
1156
|
+
},
|
|
1157
|
+
} = renderHook(() => useVisitFormSchemaAndDefaultValues(null, earliestAllowed));
|
|
1158
|
+
|
|
1159
|
+
const onBirthdate = dayjs(new Date(1990, 0, 1, 8, 0));
|
|
1160
|
+
const fields = convertToDateTimeFields(onBirthdate);
|
|
1161
|
+
|
|
1162
|
+
const { error } = visitFormSchema.safeParse({
|
|
1163
|
+
...defaultValues,
|
|
1164
|
+
visitStatus: 'ongoing',
|
|
1165
|
+
visitType: 'some-visit-type-uuid',
|
|
1166
|
+
visitStartDate: fields.date,
|
|
1167
|
+
visitStartTime: fields.time,
|
|
1168
|
+
visitStartTimeFormat: fields.timeFormat,
|
|
1169
|
+
});
|
|
1170
|
+
|
|
1171
|
+
const birthdateIssues = (error?.issues ?? []).filter((i) => i.message.includes('birth date'));
|
|
1172
|
+
expect(birthdateIssues).toHaveLength(0);
|
|
1173
|
+
});
|
|
1174
|
+
|
|
1175
|
+
it('accepts a start date within the grace period for estimated birthdates', () => {
|
|
1176
|
+
// Patient born 2015-03-01 estimated, age 10 → grace = 5 → earliest = 2010-03-01
|
|
1177
|
+
const earliestAllowed = computeEarliestAllowedStartDate('2015-03-01', true, 10);
|
|
1178
|
+
const {
|
|
1179
|
+
result: {
|
|
1180
|
+
current: { visitFormSchema, defaultValues },
|
|
1181
|
+
},
|
|
1182
|
+
} = renderHook(() => useVisitFormSchemaAndDefaultValues(null, earliestAllowed));
|
|
1183
|
+
|
|
1184
|
+
const withinGrace = dayjs(new Date(2011, 0, 1, 9, 0));
|
|
1185
|
+
const fields = convertToDateTimeFields(withinGrace);
|
|
1186
|
+
|
|
1187
|
+
const { error } = visitFormSchema.safeParse({
|
|
1188
|
+
...defaultValues,
|
|
1189
|
+
visitStatus: 'ongoing',
|
|
1190
|
+
visitType: 'some-visit-type-uuid',
|
|
1191
|
+
visitStartDate: fields.date,
|
|
1192
|
+
visitStartTime: fields.time,
|
|
1193
|
+
visitStartTimeFormat: fields.timeFormat,
|
|
1194
|
+
});
|
|
1195
|
+
|
|
1196
|
+
const birthdateIssues = (error?.issues ?? []).filter((i) => i.message.includes('birth date'));
|
|
1197
|
+
expect(birthdateIssues).toHaveLength(0);
|
|
1198
|
+
});
|
|
1199
|
+
|
|
1200
|
+
it('does not validate birthdate when earliestAllowedStartDate is null', () => {
|
|
1201
|
+
const {
|
|
1202
|
+
result: {
|
|
1203
|
+
current: { visitFormSchema, defaultValues },
|
|
1204
|
+
},
|
|
1205
|
+
} = renderHook(() => useVisitFormSchemaAndDefaultValues(null, null));
|
|
1206
|
+
|
|
1207
|
+
const veryOldDate = dayjs(new Date(1800, 0, 1, 8, 0));
|
|
1208
|
+
const fields = convertToDateTimeFields(veryOldDate);
|
|
1209
|
+
|
|
1210
|
+
const { error } = visitFormSchema.safeParse({
|
|
1211
|
+
...defaultValues,
|
|
1212
|
+
visitStatus: 'ongoing',
|
|
1213
|
+
visitType: 'some-visit-type-uuid',
|
|
1214
|
+
visitStartDate: fields.date,
|
|
1215
|
+
visitStartTime: fields.time,
|
|
1216
|
+
visitStartTimeFormat: fields.timeFormat,
|
|
1217
|
+
});
|
|
1218
|
+
|
|
1219
|
+
const birthdateIssues = (error?.issues ?? []).filter((i) => i.message.includes('birth date'));
|
|
1220
|
+
expect(birthdateIssues).toHaveLength(0);
|
|
1221
|
+
});
|
|
1222
|
+
});
|
|
1223
|
+
|
|
1224
|
+
function renderVisitForm(visitToEdit?: Visit) {
|
|
1225
|
+
const props: PatientWorkspace2DefinitionProps<VisitFormProps, {}> = {
|
|
1226
|
+
...defaultProps,
|
|
1227
|
+
groupProps: {
|
|
1228
|
+
...defaultProps.groupProps,
|
|
1229
|
+
visitContext: visitToEdit ?? null,
|
|
1230
|
+
},
|
|
1231
|
+
};
|
|
1232
|
+
return render(<VisitForm {...props} />);
|
|
1233
|
+
}
|