@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,206 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import classNames from 'classnames';
|
|
3
|
+
import dayjs from 'dayjs';
|
|
4
|
+
import { type Control, Controller, type FieldPath, useFormContext, useWatch } from 'react-hook-form';
|
|
5
|
+
import { useTranslation } from 'react-i18next';
|
|
6
|
+
import { SelectItem, TimePicker, TimePickerSelect } from '@carbon/react';
|
|
7
|
+
import { type amPm } from '@openmrs/esm-patient-common-lib';
|
|
8
|
+
import { OpenmrsDatePicker, ResponsiveWrapper } from '@openmrs/esm-framework';
|
|
9
|
+
import { convertToDate, type VisitFormData } from './visit-form.resource';
|
|
10
|
+
import styles from './visit-form.scss';
|
|
11
|
+
|
|
12
|
+
// Helpers to safely compute min/max across optional values
|
|
13
|
+
const minOf = (...values: Array<number | undefined | null>) => {
|
|
14
|
+
const nums = values.filter((v): v is number => typeof v === 'number' && Number.isFinite(v));
|
|
15
|
+
return nums.length ? Math.min(...nums) : undefined;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
const maxOf = (...values: Array<number | undefined | null>) => {
|
|
19
|
+
const nums = values.filter((v): v is number => typeof v === 'number' && Number.isFinite(v));
|
|
20
|
+
return nums.length ? Math.max(...nums) : undefined;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
interface VisitDateTimeSectionProps {
|
|
24
|
+
control: Control<VisitFormData, any>;
|
|
25
|
+
earliestStartDate?: number;
|
|
26
|
+
firstEncounterDateTime: number;
|
|
27
|
+
lastEncounterDateTime: number;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* The component conditionally renders the Visit start and end
|
|
32
|
+
* date / time fields based on the visit status (new / ongoing / past)
|
|
33
|
+
*/
|
|
34
|
+
const VisitDateTimeSection: React.FC<VisitDateTimeSectionProps> = ({
|
|
35
|
+
control,
|
|
36
|
+
earliestStartDate,
|
|
37
|
+
firstEncounterDateTime,
|
|
38
|
+
lastEncounterDateTime,
|
|
39
|
+
}) => {
|
|
40
|
+
const { t } = useTranslation();
|
|
41
|
+
const [
|
|
42
|
+
visitStatus,
|
|
43
|
+
visitStartDate,
|
|
44
|
+
visitStartTime,
|
|
45
|
+
visitStartTimeFormat,
|
|
46
|
+
visitStopDate,
|
|
47
|
+
visitStopTime,
|
|
48
|
+
visitStopTimeFormat,
|
|
49
|
+
] = useWatch({
|
|
50
|
+
control,
|
|
51
|
+
name: [
|
|
52
|
+
'visitStatus',
|
|
53
|
+
'visitStartDate',
|
|
54
|
+
'visitStartTime',
|
|
55
|
+
'visitStartTimeFormat',
|
|
56
|
+
'visitStopDate',
|
|
57
|
+
'visitStopTime',
|
|
58
|
+
'visitStopTimeFormat',
|
|
59
|
+
],
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
const hasStopTime = 'past' === visitStatus;
|
|
63
|
+
const selectedVisitStartDateTime = convertToDate(visitStartDate, visitStartTime, visitStartTimeFormat);
|
|
64
|
+
const selectedVisitStopDateTime = convertToDate(visitStopDate, visitStopTime, visitStopTimeFormat);
|
|
65
|
+
|
|
66
|
+
if (visitStatus === 'new') {
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return (
|
|
71
|
+
<section>
|
|
72
|
+
<div className={styles.sectionTitle}>
|
|
73
|
+
{visitStatus === 'ongoing'
|
|
74
|
+
? t('visitStartDate', 'Visit start date')
|
|
75
|
+
: t('visitStartAndEndDate', 'Visit start and end date')}
|
|
76
|
+
</div>
|
|
77
|
+
<VisitDateTimeField
|
|
78
|
+
dateField={{ name: 'visitStartDate', label: t('startDate', 'Start date') }}
|
|
79
|
+
timeField={{ name: 'visitStartTime', label: t('startTime', 'Start time') }}
|
|
80
|
+
timeFormatField={{ name: 'visitStartTimeFormat', label: t('startTimeFormat', 'Start time format') }}
|
|
81
|
+
minDate={earliestStartDate}
|
|
82
|
+
maxDate={minOf(Date.now(), firstEncounterDateTime, selectedVisitStopDateTime?.getTime())}
|
|
83
|
+
/>
|
|
84
|
+
{hasStopTime && (
|
|
85
|
+
<VisitDateTimeField
|
|
86
|
+
dateField={{ name: 'visitStopDate', label: t('endDate', 'End date') }}
|
|
87
|
+
timeField={{ name: 'visitStopTime', label: t('endTime', 'End time') }}
|
|
88
|
+
timeFormatField={{ name: 'visitStopTimeFormat', label: t('endTimeFormat', 'End time format') }}
|
|
89
|
+
minDate={maxOf(lastEncounterDateTime, selectedVisitStartDateTime?.getTime())}
|
|
90
|
+
maxDate={Date.now()}
|
|
91
|
+
/>
|
|
92
|
+
)}
|
|
93
|
+
</section>
|
|
94
|
+
);
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
interface VisitDateTimeFieldProps {
|
|
98
|
+
dateField: Field;
|
|
99
|
+
timeField: Field;
|
|
100
|
+
timeFormatField: Field;
|
|
101
|
+
minDate?: dayjs.ConfigType;
|
|
102
|
+
maxDate?: dayjs.ConfigType;
|
|
103
|
+
disabled?: boolean;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
interface Field {
|
|
107
|
+
name: FieldPath<VisitFormData>;
|
|
108
|
+
label: string;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* This components renders a DatePicker, TimePicker and AM / PM dropdown
|
|
113
|
+
* used to input a Date.
|
|
114
|
+
* It is used by the visit form for the start and end time inputs.
|
|
115
|
+
*/
|
|
116
|
+
const VisitDateTimeField: React.FC<VisitDateTimeFieldProps> = ({
|
|
117
|
+
dateField,
|
|
118
|
+
timeField,
|
|
119
|
+
timeFormatField,
|
|
120
|
+
minDate,
|
|
121
|
+
maxDate,
|
|
122
|
+
disabled,
|
|
123
|
+
}) => {
|
|
124
|
+
const {
|
|
125
|
+
control,
|
|
126
|
+
formState: { errors },
|
|
127
|
+
} = useFormContext<VisitFormData>();
|
|
128
|
+
const { t } = useTranslation();
|
|
129
|
+
|
|
130
|
+
// Since we have the separate date and time fields, the full validation is done by zod.
|
|
131
|
+
// We are just using minDateObj and maxDateObj to restrict the bounds of the DatePicker.
|
|
132
|
+
const minDateObj = minDate ? dayjs(minDate).startOf('day') : null;
|
|
133
|
+
const maxDateObj = maxDate ? dayjs(maxDate).endOf('day') : null;
|
|
134
|
+
|
|
135
|
+
return (
|
|
136
|
+
<div className={classNames(styles.dateTimeSection, styles.sectionField)}>
|
|
137
|
+
<Controller
|
|
138
|
+
name={dateField.name}
|
|
139
|
+
control={control}
|
|
140
|
+
render={({ field, fieldState }) => (
|
|
141
|
+
<ResponsiveWrapper>
|
|
142
|
+
<OpenmrsDatePicker
|
|
143
|
+
{...field}
|
|
144
|
+
value={field.value as Date}
|
|
145
|
+
className={styles.datePicker}
|
|
146
|
+
id={`${dateField.name}Input`}
|
|
147
|
+
data-testid={`${dateField.name}Input`}
|
|
148
|
+
maxDate={maxDateObj}
|
|
149
|
+
minDate={minDateObj}
|
|
150
|
+
labelText={dateField.label}
|
|
151
|
+
invalid={Boolean(fieldState?.error?.message)}
|
|
152
|
+
invalidText={fieldState?.error?.message}
|
|
153
|
+
/>
|
|
154
|
+
</ResponsiveWrapper>
|
|
155
|
+
)}
|
|
156
|
+
/>
|
|
157
|
+
<ResponsiveWrapper>
|
|
158
|
+
<Controller
|
|
159
|
+
name={timeField.name}
|
|
160
|
+
control={control}
|
|
161
|
+
render={({ field: { onBlur, onChange, value } }) => (
|
|
162
|
+
<div className={styles.timePickerContainer}>
|
|
163
|
+
<TimePicker
|
|
164
|
+
className={styles.timePicker}
|
|
165
|
+
disabled={disabled}
|
|
166
|
+
id={timeField.name}
|
|
167
|
+
invalid={Boolean(errors[timeField.name])}
|
|
168
|
+
invalidText={errors[timeField.name]?.message}
|
|
169
|
+
labelText={timeField.label}
|
|
170
|
+
onBlur={onBlur}
|
|
171
|
+
onChange={(event) => onChange(event.target.value)}
|
|
172
|
+
pattern="^(0[1-9]|1[0-2]):([0-5][0-9])$"
|
|
173
|
+
value={value as string}
|
|
174
|
+
>
|
|
175
|
+
<Controller
|
|
176
|
+
name={timeFormatField.name}
|
|
177
|
+
control={control}
|
|
178
|
+
render={({ field: { onChange, value } }) => (
|
|
179
|
+
<TimePickerSelect
|
|
180
|
+
aria-label={timeFormatField.label}
|
|
181
|
+
className={classNames({
|
|
182
|
+
[styles.timePickerSelectError]: errors[timeFormatField.name],
|
|
183
|
+
})}
|
|
184
|
+
disabled={disabled}
|
|
185
|
+
id={`${timeFormatField.name}Input`}
|
|
186
|
+
onChange={(event) => onChange(event.target.value as amPm)}
|
|
187
|
+
value={value as amPm}
|
|
188
|
+
>
|
|
189
|
+
<SelectItem value="AM" text={t('AM', 'AM')} />
|
|
190
|
+
<SelectItem value="PM" text={t('PM', 'PM')} />
|
|
191
|
+
</TimePickerSelect>
|
|
192
|
+
)}
|
|
193
|
+
/>
|
|
194
|
+
</TimePicker>
|
|
195
|
+
{errors[timeFormatField.name] && (
|
|
196
|
+
<div className={styles.timerPickerError}>{errors[timeFormatField.name]?.message}</div>
|
|
197
|
+
)}
|
|
198
|
+
</div>
|
|
199
|
+
)}
|
|
200
|
+
/>
|
|
201
|
+
</ResponsiveWrapper>
|
|
202
|
+
</div>
|
|
203
|
+
);
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
export default VisitDateTimeSection;
|
|
@@ -0,0 +1,401 @@
|
|
|
1
|
+
import { useMemo, useState } from 'react';
|
|
2
|
+
import { useTranslation } from 'react-i18next';
|
|
3
|
+
import type { TFunction } from 'i18next';
|
|
4
|
+
import dayjs from 'dayjs';
|
|
5
|
+
import useSWRImmutable from 'swr/immutable';
|
|
6
|
+
import { z } from 'zod';
|
|
7
|
+
import {
|
|
8
|
+
type FetchResponse,
|
|
9
|
+
openmrsFetch,
|
|
10
|
+
restBaseUrl,
|
|
11
|
+
useConfig,
|
|
12
|
+
useConnectivity,
|
|
13
|
+
useEmrConfiguration,
|
|
14
|
+
useFeatureFlag,
|
|
15
|
+
useSession,
|
|
16
|
+
useVisitTypes,
|
|
17
|
+
type Visit,
|
|
18
|
+
} from '@openmrs/esm-framework';
|
|
19
|
+
import { time12HourFormatRegex, type amPm } from '@openmrs/esm-patient-common-lib';
|
|
20
|
+
import { useDefaultVisitLocation } from '../hooks/useDefaultVisitLocation';
|
|
21
|
+
import { useOfflineVisitType } from '../hooks/useOfflineVisitType';
|
|
22
|
+
import { type ChartConfig } from '../../config-schema';
|
|
23
|
+
|
|
24
|
+
export const visitStatuses = ['new', 'ongoing', 'past'] as const;
|
|
25
|
+
export type VisitStatus = (typeof visitStatuses)[number];
|
|
26
|
+
|
|
27
|
+
export type VisitFormData = {
|
|
28
|
+
visitStatus: VisitStatus;
|
|
29
|
+
visitStartDate: Date; // Date object that only contains info for year, month, day
|
|
30
|
+
visitStartTime: string; // hh:mm (note that hh is from 01 to 12, NOT 00 to 23)
|
|
31
|
+
visitStartTimeFormat: amPm;
|
|
32
|
+
visitStopDate: Date; // Date object that only contains info for year, month, day
|
|
33
|
+
visitStopTime: string; // hh:mm (note that hh is from 01 to 12, NOT 00 to 23)
|
|
34
|
+
visitStopTimeFormat: amPm;
|
|
35
|
+
programType: string;
|
|
36
|
+
visitType: string;
|
|
37
|
+
visitLocation: {
|
|
38
|
+
display?: string;
|
|
39
|
+
uuid?: string;
|
|
40
|
+
};
|
|
41
|
+
visitAttributes: {
|
|
42
|
+
[x: string]: string;
|
|
43
|
+
};
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
// *****************
|
|
47
|
+
// Copied from form-submission.service.ts
|
|
48
|
+
// TODO: consolidate logic for parsing errors from REST API calls
|
|
49
|
+
export type FieldError = {
|
|
50
|
+
[key: string]: Array<{ code: string; message: string }>;
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
export type ErrorObject = {
|
|
54
|
+
error: {
|
|
55
|
+
code: string;
|
|
56
|
+
message: string;
|
|
57
|
+
detail: string;
|
|
58
|
+
fieldErrors?: FieldError;
|
|
59
|
+
globalErrors?: Array<{ code: string; message: string }>;
|
|
60
|
+
};
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
export function extractErrorMessagesFromResponse(errorObject: ErrorObject, t: TFunction) {
|
|
64
|
+
const { fieldErrors, globalErrors, message, code } = errorObject?.error ?? {};
|
|
65
|
+
|
|
66
|
+
if (fieldErrors && Object.keys(fieldErrors).length > 0) {
|
|
67
|
+
return Object.values(fieldErrors)
|
|
68
|
+
.flatMap((errors) => errors.map((error) => error.message))
|
|
69
|
+
.join('\n');
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (globalErrors?.length > 0) {
|
|
73
|
+
return globalErrors.map((error) => error.message).join('\n');
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return message ?? code ?? t('unknownError', 'Unknown error');
|
|
77
|
+
}
|
|
78
|
+
// *****************
|
|
79
|
+
|
|
80
|
+
export function useConditionalVisitTypes() {
|
|
81
|
+
const isOnline = useConnectivity();
|
|
82
|
+
|
|
83
|
+
const visitTypesHook = isOnline ? useVisitTypes : useOfflineVisitType;
|
|
84
|
+
|
|
85
|
+
return visitTypesHook();
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
interface PatientPersonResponse {
|
|
89
|
+
person: {
|
|
90
|
+
birthdate: string | null;
|
|
91
|
+
birthdateEstimated: boolean;
|
|
92
|
+
age: number | null;
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const patientPersonCustomRep = 'custom:(person:(birthdate,birthdateEstimated,age))';
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Parses a date string into a local-midnight Date,
|
|
100
|
+
* avoiding timezone drift from `new Date(str)` or dayjs(str).
|
|
101
|
+
* Accepts both 'YYYY-MM-DD' and full ISO datetime strings
|
|
102
|
+
* like '1979-12-08T00:00:00.000+0530' (as returned by the REST API).
|
|
103
|
+
*/
|
|
104
|
+
function parseLocalDate(dateStr: string): Date | null {
|
|
105
|
+
const datePart = dateStr.split('T')[0];
|
|
106
|
+
const parts = datePart.split('-');
|
|
107
|
+
if (parts.length !== 3) {
|
|
108
|
+
return null;
|
|
109
|
+
}
|
|
110
|
+
const [y, m, d] = parts.map(Number);
|
|
111
|
+
return new Date(y, m - 1, d);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Computes the earliest allowed visit start date based on the patient's birthdate.
|
|
116
|
+
* For estimated birthdates, a grace period shifts the boundary earlier,
|
|
117
|
+
* matching the backend's VisitValidator logic.
|
|
118
|
+
*/
|
|
119
|
+
export function computeEarliestAllowedStartDate(
|
|
120
|
+
birthdate: string | null,
|
|
121
|
+
birthdateEstimated: boolean,
|
|
122
|
+
age: number | null,
|
|
123
|
+
): Date | null {
|
|
124
|
+
if (!birthdate) {
|
|
125
|
+
return null;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const earliest = parseLocalDate(birthdate);
|
|
129
|
+
if (!earliest) {
|
|
130
|
+
return null;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// When the birthdate is estimated, the backend's VisitValidator applies a grace period
|
|
134
|
+
// that shifts the earliest allowed date further into the past. The grace is half the
|
|
135
|
+
// patient's age (in years), with a minimum of 1 year.
|
|
136
|
+
// @see https://github.com/openmrs/openmrs-core/blob/master/api/src/main/java/org/openmrs/validator/VisitValidator.java
|
|
137
|
+
if (birthdateEstimated && age != null) {
|
|
138
|
+
const graceYears = Math.max(1, Math.floor(age * 0.5));
|
|
139
|
+
earliest.setFullYear(earliest.getFullYear() - graceYears);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return earliest;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// We need a separate REST call here because the FHIR Patient resource doesn't
|
|
146
|
+
// include `birthdateEstimated` or `age`, which are required for the grace period
|
|
147
|
+
// calculation. The FHIR patient's `birthDate` alone isn't sufficient.
|
|
148
|
+
export function useEarliestAllowedVisitStartDate(patientUuid: string) {
|
|
149
|
+
const { data, isLoading } = useSWRImmutable<FetchResponse<PatientPersonResponse>>(
|
|
150
|
+
`${restBaseUrl}/patient/${patientUuid}?v=${patientPersonCustomRep}`,
|
|
151
|
+
openmrsFetch,
|
|
152
|
+
);
|
|
153
|
+
|
|
154
|
+
const earliestAllowedStartDate = useMemo(() => {
|
|
155
|
+
if (!data?.data?.person) {
|
|
156
|
+
return null;
|
|
157
|
+
}
|
|
158
|
+
const { birthdate, birthdateEstimated, age } = data.data.person;
|
|
159
|
+
return computeEarliestAllowedStartDate(birthdate, birthdateEstimated, age);
|
|
160
|
+
}, [data]);
|
|
161
|
+
|
|
162
|
+
return { earliestAllowedStartDate, isLoading };
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
export function useAllowOverlappingVisits() {
|
|
166
|
+
const isOnline = useConnectivity();
|
|
167
|
+
const { data, error, isLoading } = useSWRImmutable<FetchResponse<{ value: string }>>(
|
|
168
|
+
isOnline ? `${restBaseUrl}/systemsetting/visits.allowOverlappingVisits?v=custom:(value)` : null,
|
|
169
|
+
openmrsFetch,
|
|
170
|
+
);
|
|
171
|
+
return {
|
|
172
|
+
allowOverlappingVisits: error || !data ? true : (data.data.value ?? 'true').toLowerCase() === 'true',
|
|
173
|
+
isLoading,
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
export interface VisitFormCallbacks {
|
|
178
|
+
onVisitCreatedOrUpdated: (visit: Visit) => Promise<any>;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
export function useVisitFormCallbacks() {
|
|
182
|
+
return useState<Map<string, VisitFormCallbacks>>(new Map());
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
export function createVisitAttribute(visitUuid: string, attributeType: string, value: string) {
|
|
186
|
+
return openmrsFetch(`${restBaseUrl}/visit/${visitUuid}/attribute`, {
|
|
187
|
+
method: 'POST',
|
|
188
|
+
headers: { 'Content-type': 'application/json' },
|
|
189
|
+
body: { attributeType, value },
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
export function updateVisitAttribute(visitUuid: string, visitAttributeUuid: string, value: string) {
|
|
194
|
+
return openmrsFetch(`${restBaseUrl}/visit/${visitUuid}/attribute/${visitAttributeUuid}`, {
|
|
195
|
+
method: 'POST',
|
|
196
|
+
headers: { 'Content-type': 'application/json' },
|
|
197
|
+
body: { value },
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
export function deleteVisitAttribute(visitUuid: string, visitAttributeUuid: string) {
|
|
202
|
+
return openmrsFetch(`${restBaseUrl}/visit/${visitUuid}/attribute/${visitAttributeUuid}`, {
|
|
203
|
+
method: 'DELETE',
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
export function useVisitFormSchemaAndDefaultValues(visitToEdit: Visit, earliestAllowedStartDate?: Date | null) {
|
|
208
|
+
const { t } = useTranslation();
|
|
209
|
+
const { visitAttributeTypes, restrictByVisitLocationTag } = useConfig<ChartConfig>();
|
|
210
|
+
const isEmrApiModuleInstalled = useFeatureFlag('emrapi-module');
|
|
211
|
+
const sessionUser = useSession();
|
|
212
|
+
const sessionLocation = sessionUser?.sessionLocation;
|
|
213
|
+
const defaultVisitLocation = useDefaultVisitLocation(
|
|
214
|
+
sessionLocation,
|
|
215
|
+
restrictByVisitLocationTag && isEmrApiModuleInstalled,
|
|
216
|
+
);
|
|
217
|
+
const { emrConfiguration } = useEmrConfiguration();
|
|
218
|
+
|
|
219
|
+
return useMemo(() => {
|
|
220
|
+
const now = new Date();
|
|
221
|
+
|
|
222
|
+
const allEncounterDateTimes = (visitToEdit?.encounters ?? []).map(({ encounterDatetime }) =>
|
|
223
|
+
Date.parse(encounterDatetime),
|
|
224
|
+
);
|
|
225
|
+
|
|
226
|
+
const firstEncounterDateTime: number = Math.min(...allEncounterDateTimes);
|
|
227
|
+
const lastEncounterDateTime: number = Math.max(...allEncounterDateTimes);
|
|
228
|
+
|
|
229
|
+
const startDateTime = convertToDateTimeFields(visitToEdit?.startDatetime ?? now);
|
|
230
|
+
const stopDateTime = convertToDateTimeFields(visitToEdit?.stopDatetime ?? now);
|
|
231
|
+
|
|
232
|
+
const visitStatus: VisitStatus =
|
|
233
|
+
visitToEdit == null ? 'new' : visitToEdit.stopDatetime === null ? 'ongoing' : 'past';
|
|
234
|
+
|
|
235
|
+
const defaultValues: Partial<VisitFormData> = {
|
|
236
|
+
visitStatus,
|
|
237
|
+
visitStartDate: startDateTime.date,
|
|
238
|
+
visitStartTime: startDateTime.time,
|
|
239
|
+
visitStartTimeFormat: startDateTime.timeFormat,
|
|
240
|
+
visitStopDate: stopDateTime.date,
|
|
241
|
+
visitStopTime: stopDateTime.time,
|
|
242
|
+
visitStopTimeFormat: stopDateTime.timeFormat,
|
|
243
|
+
visitType: visitToEdit?.visitType?.uuid ?? emrConfiguration?.atFacilityVisitType?.uuid,
|
|
244
|
+
visitLocation: visitToEdit?.location ?? defaultVisitLocation ?? {},
|
|
245
|
+
visitAttributes:
|
|
246
|
+
visitToEdit?.attributes.reduce(
|
|
247
|
+
(acc, curr) => ({
|
|
248
|
+
...acc,
|
|
249
|
+
[curr.attributeType.uuid]: typeof curr.value === 'object' ? curr?.value?.uuid : `${curr.value ?? ''}`,
|
|
250
|
+
}),
|
|
251
|
+
{},
|
|
252
|
+
) ?? {},
|
|
253
|
+
};
|
|
254
|
+
|
|
255
|
+
const visitAttributes = (visitAttributeTypes ?? [])?.reduce(
|
|
256
|
+
(acc, { uuid, required }) => ({
|
|
257
|
+
...acc,
|
|
258
|
+
[uuid]: required
|
|
259
|
+
? z.string({ required_error: t('fieldRequired', 'This field is required') })
|
|
260
|
+
: z.string().optional(),
|
|
261
|
+
}),
|
|
262
|
+
{},
|
|
263
|
+
);
|
|
264
|
+
|
|
265
|
+
const visitStatusEnum = z.enum(visitStatuses);
|
|
266
|
+
const visitFormSchema = z
|
|
267
|
+
.object({
|
|
268
|
+
visitStatus: visitToEdit ? visitStatusEnum.exclude(['new']) : visitStatusEnum,
|
|
269
|
+
visitStartDate: z.date().optional(),
|
|
270
|
+
visitStartTime: z.string().regex(time12HourFormatRegex).optional(),
|
|
271
|
+
visitStartTimeFormat: z.enum(['PM', 'AM']).optional(),
|
|
272
|
+
visitStopDate: z.date().optional(),
|
|
273
|
+
visitStopTime: z.string().regex(time12HourFormatRegex).optional(),
|
|
274
|
+
visitStopTimeFormat: z.enum(['PM', 'AM']).optional(),
|
|
275
|
+
programType: z.string().optional(),
|
|
276
|
+
visitType: z.string({ required_error: t('visitTypeRequired', 'Visit type is required') }),
|
|
277
|
+
visitLocation: z.object({
|
|
278
|
+
display: z.string(),
|
|
279
|
+
uuid: z.string({ required_error: t('visitLocationRequired', 'Visit location is required') }),
|
|
280
|
+
}),
|
|
281
|
+
visitAttributes: z.object(visitAttributes),
|
|
282
|
+
})
|
|
283
|
+
.superRefine((data, ctx) => {
|
|
284
|
+
const {
|
|
285
|
+
visitStatus,
|
|
286
|
+
visitStartDate,
|
|
287
|
+
visitStartTime,
|
|
288
|
+
visitStartTimeFormat,
|
|
289
|
+
visitStopDate,
|
|
290
|
+
visitStopTime,
|
|
291
|
+
visitStopTimeFormat,
|
|
292
|
+
} = data;
|
|
293
|
+
|
|
294
|
+
const visitStartDateTime = convertToDate(visitStartDate, visitStartTime, visitStartTimeFormat);
|
|
295
|
+
const visitStopDateTime = convertToDate(visitStopDate, visitStopTime, visitStopTimeFormat);
|
|
296
|
+
|
|
297
|
+
if (visitStatus === 'ongoing' || visitStatus === 'past') {
|
|
298
|
+
if (visitStartDateTime === null) {
|
|
299
|
+
ctx.addIssue({
|
|
300
|
+
code: z.ZodIssueCode.custom,
|
|
301
|
+
message: t('visitStartDateTimeRequired', 'Start date and time are required'),
|
|
302
|
+
path: ['visitStartDate'],
|
|
303
|
+
});
|
|
304
|
+
} else if (earliestAllowedStartDate && visitStartDateTime < earliestAllowedStartDate) {
|
|
305
|
+
ctx.addIssue({
|
|
306
|
+
code: z.ZodIssueCode.custom,
|
|
307
|
+
message: t('visitStartDateBeforeBirthdate', "Start date cannot be before the patient's birth date"),
|
|
308
|
+
path: ['visitStartDate'],
|
|
309
|
+
});
|
|
310
|
+
} else if (visitStartDateTime > now) {
|
|
311
|
+
ctx.addIssue({
|
|
312
|
+
code: z.ZodIssueCode.custom,
|
|
313
|
+
message: t('futureStartTime', 'Start time cannot be in the future'),
|
|
314
|
+
path: ['visitStartTime'],
|
|
315
|
+
});
|
|
316
|
+
} else if (visitStartDateTime.getTime() > firstEncounterDateTime) {
|
|
317
|
+
ctx.addIssue({
|
|
318
|
+
code: z.ZodIssueCode.custom,
|
|
319
|
+
message: t(
|
|
320
|
+
'visitStartDateMustBeBeforeEarliestEncounter',
|
|
321
|
+
'Start time must be on or before {{firstEncounterDatetime}}',
|
|
322
|
+
{
|
|
323
|
+
firstEncounterDatetime: new Date(firstEncounterDateTime).toLocaleString(),
|
|
324
|
+
interpolation: {
|
|
325
|
+
escapeValue: false,
|
|
326
|
+
},
|
|
327
|
+
},
|
|
328
|
+
),
|
|
329
|
+
path: ['visitStartTime'],
|
|
330
|
+
});
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
if (visitStatus === 'past') {
|
|
335
|
+
if (visitStopDateTime === null) {
|
|
336
|
+
ctx.addIssue({
|
|
337
|
+
code: z.ZodIssueCode.custom,
|
|
338
|
+
message: t('endDateTimeRequired', 'End date and time are required'),
|
|
339
|
+
path: ['visitStopDate'],
|
|
340
|
+
});
|
|
341
|
+
} else if (visitStopDateTime > now) {
|
|
342
|
+
ctx.addIssue({
|
|
343
|
+
code: z.ZodIssueCode.custom,
|
|
344
|
+
message: t('futureEndTime', 'End time cannot be in the future'),
|
|
345
|
+
path: ['visitStopTime'],
|
|
346
|
+
});
|
|
347
|
+
} else if (visitStopDateTime < visitStartDateTime) {
|
|
348
|
+
ctx.addIssue({
|
|
349
|
+
code: z.ZodIssueCode.custom,
|
|
350
|
+
message: t('endTimeMustBeAfterStartTime', 'End time must be after start time'),
|
|
351
|
+
path: ['visitStopDate'],
|
|
352
|
+
});
|
|
353
|
+
} else if (visitStopDateTime.getTime() < lastEncounterDateTime) {
|
|
354
|
+
ctx.addIssue({
|
|
355
|
+
code: z.ZodIssueCode.custom,
|
|
356
|
+
message: t(
|
|
357
|
+
'endTimeMustBeAfterMostRecentEncounter',
|
|
358
|
+
'End time must be on or after {{lastEncounterDatetime}}',
|
|
359
|
+
{
|
|
360
|
+
lastEncounterDatetime: new Date(lastEncounterDateTime).toLocaleString(),
|
|
361
|
+
interpolation: {
|
|
362
|
+
escapeValue: false,
|
|
363
|
+
},
|
|
364
|
+
},
|
|
365
|
+
),
|
|
366
|
+
path: ['visitStopTime'],
|
|
367
|
+
});
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
});
|
|
371
|
+
|
|
372
|
+
return { visitFormSchema, defaultValues, firstEncounterDateTime, lastEncounterDateTime };
|
|
373
|
+
}, [t, visitAttributeTypes, visitToEdit, defaultVisitLocation, emrConfiguration, earliestAllowedStartDate]);
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
// Returns a Date object based on date, time and am/pm inputs from user.
|
|
377
|
+
// Note that the inputs are expected to be in local time.
|
|
378
|
+
// Returns a non-null Date only is the inputs are valid
|
|
379
|
+
export const convertToDate = (
|
|
380
|
+
date: Date, // Date object that only contains info for year, month, day
|
|
381
|
+
time12h: string, // hh:mm, where hh is 01 to 12
|
|
382
|
+
timeFormat: amPm, // AM / PM
|
|
383
|
+
): Date | null => {
|
|
384
|
+
if (!date || !time12h || !timeFormat) {
|
|
385
|
+
return null;
|
|
386
|
+
}
|
|
387
|
+
const dateStr = dayjs(date).format('YYYY-MM-DD');
|
|
388
|
+
const ret = dayjs(`${dateStr} ${time12h} ${timeFormat}`, 'YYYY-MM-DD hh:mm A');
|
|
389
|
+
return ret.isValid() ? ret.toDate() : null;
|
|
390
|
+
};
|
|
391
|
+
|
|
392
|
+
// The inverse of `convertToDate`. Takes a Date-like value and returns
|
|
393
|
+
// the date (with hour / minute / seconds truncated), time12h and timeFormat
|
|
394
|
+
export const convertToDateTimeFields = (dateTime: dayjs.ConfigType) => {
|
|
395
|
+
const dateTimeDayjs = dayjs(dateTime);
|
|
396
|
+
return {
|
|
397
|
+
date: dateTimeDayjs.startOf('day').toDate(),
|
|
398
|
+
time: dateTimeDayjs.format('hh:mm'),
|
|
399
|
+
timeFormat: dateTimeDayjs.format('A') as amPm,
|
|
400
|
+
};
|
|
401
|
+
};
|