@kenyaemr/esm-imaging-orders-app 4.0.1-pre.1

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.
Files changed (110) hide show
  1. package/.turbo/turbo-build.log +188 -0
  2. package/README.md +8 -0
  3. package/dist/123.js +2 -0
  4. package/dist/123.js.LICENSE.txt +40 -0
  5. package/dist/123.js.map +1 -0
  6. package/dist/144.js +2 -0
  7. package/dist/144.js.LICENSE.txt +19 -0
  8. package/dist/144.js.map +1 -0
  9. package/dist/225.js +1 -0
  10. package/dist/225.js.map +1 -0
  11. package/dist/300.js +1 -0
  12. package/dist/364.js +1 -0
  13. package/dist/364.js.map +1 -0
  14. package/dist/372.js +1 -0
  15. package/dist/372.js.map +1 -0
  16. package/dist/41.js +2 -0
  17. package/dist/41.js.LICENSE.txt +9 -0
  18. package/dist/41.js.map +1 -0
  19. package/dist/495.js +1 -0
  20. package/dist/495.js.map +1 -0
  21. package/dist/606.js +1 -0
  22. package/dist/606.js.map +1 -0
  23. package/dist/831.js +2 -0
  24. package/dist/831.js.LICENSE.txt +5 -0
  25. package/dist/831.js.map +1 -0
  26. package/dist/876.js +2 -0
  27. package/dist/876.js.LICENSE.txt +9 -0
  28. package/dist/876.js.map +1 -0
  29. package/dist/913.js +2 -0
  30. package/dist/913.js.LICENSE.txt +32 -0
  31. package/dist/913.js.map +1 -0
  32. package/dist/kenyaemr-esm-imaging-orders-app.js +1 -0
  33. package/dist/kenyaemr-esm-imaging-orders-app.js.buildmanifest.json +401 -0
  34. package/dist/kenyaemr-esm-imaging-orders-app.js.map +1 -0
  35. package/dist/main.js +2 -0
  36. package/dist/main.js.LICENSE.txt +60 -0
  37. package/dist/main.js.map +1 -0
  38. package/dist/routes.json +1 -0
  39. package/jest.config.js +8 -0
  40. package/package.json +55 -0
  41. package/src/config-schema.ts +51 -0
  42. package/src/constants.ts +3 -0
  43. package/src/declarations.d.ts +6 -0
  44. package/src/form/imaging-orders/add-imaging-orders/add-imaging-order.scss +44 -0
  45. package/src/form/imaging-orders/add-imaging-orders/add-imaging-order.workspace.tsx +86 -0
  46. package/src/form/imaging-orders/add-imaging-orders/imaging-order-form.component.tsx +354 -0
  47. package/src/form/imaging-orders/add-imaging-orders/imaging-order-form.scss +79 -0
  48. package/src/form/imaging-orders/add-imaging-orders/imaging-order.ts +19 -0
  49. package/src/form/imaging-orders/add-imaging-orders/imaging-type-search.scss +115 -0
  50. package/src/form/imaging-orders/add-imaging-orders/imaging-type-search.tsx +235 -0
  51. package/src/form/imaging-orders/add-imaging-orders/useImagingTypes.ts +89 -0
  52. package/src/form/imaging-orders/api.ts +230 -0
  53. package/src/form/imaging-orders/imaging-order-basket-panel/imaging-icon.component.tsx +40 -0
  54. package/src/form/imaging-orders/imaging-order-basket-panel/imaging-order-basket-item-tile.component.tsx +96 -0
  55. package/src/form/imaging-orders/imaging-order-basket-panel/imaging-order-basket-item-tile.scss +72 -0
  56. package/src/form/imaging-orders/imaging-order-basket-panel/imaging-order-basket-panel.extension.tsx +191 -0
  57. package/src/form/imaging-orders/imaging-order-basket-panel/imaging-order-basket-panel.scss +74 -0
  58. package/src/form/imaging-orders/useOrderConfig.ts +48 -0
  59. package/src/form/imaging-report-form/imaging-report-form.component.tsx +161 -0
  60. package/src/form/imaging-report-form/imaging-report-form.scss +30 -0
  61. package/src/form/imaging-report-form/imaging.resource.ts +360 -0
  62. package/src/header/imagining-header.component.tsx +17 -0
  63. package/src/header/imagining-header.scss +5 -0
  64. package/src/hooks/useOrdersWorklist.ts +59 -0
  65. package/src/hooks/useSearchGroupedResults.ts +27 -0
  66. package/src/hooks/useSearchResults.ts +51 -0
  67. package/src/imaging-orders.component.tsx +14 -0
  68. package/src/imaging-tabs/approved/approved-orders.component.tsx +31 -0
  69. package/src/imaging-tabs/approved/approved-orders.scss +0 -0
  70. package/src/imaging-tabs/imaging-tabs.component.tsx +79 -0
  71. package/src/imaging-tabs/imaging-tabs.scss +5 -0
  72. package/src/imaging-tabs/orders-not-done/orders-not-done.component.tsx +42 -0
  73. package/src/imaging-tabs/referred-test/referred-ordered.component.tsx +26 -0
  74. package/src/imaging-tabs/referred-test/referred-ordered.scss +6 -0
  75. package/src/imaging-tabs/review-ordered/review-imaging-report-modal/review-imaging-report-dialog.component.tsx +138 -0
  76. package/src/imaging-tabs/review-ordered/review-imaging-report-modal/review-imaging-report-dialog.scss +5 -0
  77. package/src/imaging-tabs/review-ordered/review-ordered.component.tsx +28 -0
  78. package/src/imaging-tabs/review-ordered/review-ordered.scss +0 -0
  79. package/src/imaging-tabs/test-ordered/pick-imaging-order/add-to-worklist-dialog.component.tsx +94 -0
  80. package/src/imaging-tabs/test-ordered/pick-imaging-order/add-to-worklist-dialog.resource.ts +137 -0
  81. package/src/imaging-tabs/test-ordered/pick-imaging-order/add-to-worklist-dialog.scss +38 -0
  82. package/src/imaging-tabs/test-ordered/reject-order-dialog/radiology-reject-reason.component.tsx +40 -0
  83. package/src/imaging-tabs/test-ordered/reject-order-dialog/reject-order-dialog.component.tsx +95 -0
  84. package/src/imaging-tabs/test-ordered/reject-order-dialog/reject-order-dialog.scss +14 -0
  85. package/src/imaging-tabs/test-ordered/tests-ordered.component.tsx +35 -0
  86. package/src/imaging-tabs/test-ordered/tests-ordered.scss +13 -0
  87. package/src/imaging-tabs/test-ordered/transition-patient-new-queue/transition-latest-queue-entry-button.component.tsx +34 -0
  88. package/src/imaging-tabs/test-ordered/transition-patient-new-queue/transition-latest-queue-entry-button.scss +14 -0
  89. package/src/imaging-tabs/work-list/work-list.component.tsx +45 -0
  90. package/src/imaging-tabs/work-list/work-list.resource.ts +150 -0
  91. package/src/imaging-tabs/work-list/work-list.scss +207 -0
  92. package/src/index.ts +45 -0
  93. package/src/left-panel-link.tsx +42 -0
  94. package/src/root.component.tsx +19 -0
  95. package/src/routes.json +58 -0
  96. package/src/shared/imaging.resource.tsx +65 -0
  97. package/src/shared/ui/common/action-button/action-button.component.tsx +66 -0
  98. package/src/shared/ui/common/action-button/order-action-extension.component.tsx +21 -0
  99. package/src/shared/ui/common/grouped-imaging-types.ts +48 -0
  100. package/src/shared/ui/common/grouped-orders-table.component.tsx +154 -0
  101. package/src/shared/ui/common/grouped-orders-table.scss +13 -0
  102. package/src/shared/ui/common/list-order-details.component.tsx +72 -0
  103. package/src/shared/ui/common/list-order-details.scss +52 -0
  104. package/src/shared/ui/common/order-detail.component.tsx +14 -0
  105. package/src/shared/ui/common/order-detail.scss +14 -0
  106. package/src/types/index.ts +49 -0
  107. package/src/utils/functions.ts +238 -0
  108. package/translations/en.json +69 -0
  109. package/tsconfig.json +5 -0
  110. package/webpack.config.js +1 -0
@@ -0,0 +1 @@
1
+ {"$schema":"https://json.openmrs.org/routes.schema.json","backendDependencies":{"fhir2":">=1.2","webservices.rest":"^2.24.0"},"extensions":[{"component":"imagingOrdersLink","name":"imaging-orders-link","slot":"homepage-dashboard-slot","meta":{"name":"imaging-orders","title":"imaging-orders","slot":"imaging-dashboard-slot"}},{"component":"root","name":"imaging-dashboard-root","slot":"imaging-dashboard-slot"},{"name":"imaging-order-panel","component":"imagingOrderPanel","slot":"order-basket-slot","order":3}],"workspaces":[{"name":"add-imaging-order","type":"order","component":"addImagingOrderWorkspace","title":"Add Imaging order"},{"name":"imaging-report-form","component":"imagingReportForm","title":"Imaging Report Form","type":"form"}],"modals":[{"name":"review-imaging-report-modal","component":"reviewImagingReportModal"},{"name":"reject-imaging-order-modal","component":"rejectImagingOrderModal"},{"name":"add-imaging-to-work-list-modal","component":"addImagingToWorkListModal"}],"version":"4.0.1-pre.1"}
package/jest.config.js ADDED
@@ -0,0 +1,8 @@
1
+ const rootConfig = require('../../jest.config.js');
2
+
3
+ const packageConfig = {
4
+ ...rootConfig,
5
+ collectCoverage: false,
6
+ };
7
+
8
+ module.exports = packageConfig;
package/package.json ADDED
@@ -0,0 +1,55 @@
1
+ {
2
+ "name": "@kenyaemr/esm-imaging-orders-app",
3
+ "version": "4.0.1-pre.1",
4
+ "description": "Imaging Orders app for KenyaEMR",
5
+ "browser": "dist/kenyaemr-esm-imaging-orders-app.js",
6
+ "main": "src/index.ts",
7
+ "source": true,
8
+ "license": "MPL-2.0",
9
+ "homepage": "https://github.com/palladiumkenya/kenyaemr-esm-orders#readme",
10
+ "scripts": {
11
+ "start": "openmrs develop",
12
+ "serve": "webpack serve --mode=development",
13
+ "debug": "npm run serve",
14
+ "build": "webpack --mode production",
15
+ "analyze": "webpack --mode=production --env.analyze=true",
16
+ "lint": "eslint src --ext ts,tsx",
17
+ "typescript": "tsc",
18
+ "extract-translations": "i18next 'src/**/*.component.tsx' 'src/index.ts' --config ../../tools/i18next-parser.config.js",
19
+ "test": "cross-env TZ=UTC jest --config jest.config.js --verbose false --passWithNoTests",
20
+ "test:watch": "cross-env TZ=UTC jest --watch --config jest.config.js",
21
+ "coverage": "yarn test --coverage"
22
+ },
23
+ "browserslist": [
24
+ "extends browserslist-config-openmrs"
25
+ ],
26
+ "keywords": [
27
+ "openmrs"
28
+ ],
29
+ "publishConfig": {
30
+ "access": "public"
31
+ },
32
+ "repository": {
33
+ "type": "git",
34
+ "url": "git+https://github.com/palladiumkenya/kenyaemr-esm-orders#readme"
35
+ },
36
+ "bugs": {
37
+ "url": "https://github.com/palladiumkenya/kenyaemr-esm-orders/issues"
38
+ },
39
+ "dependencies": {
40
+ "@carbon/react": "^1.42.1",
41
+ "lodash-es": "^4.17.15",
42
+ "react-to-print": "^2.14.13"
43
+ },
44
+ "peerDependencies": {
45
+ "@openmrs/esm-framework": "5.x",
46
+ "react": "^18.1.0",
47
+ "react-i18next": "11.x",
48
+ "react-router-dom": "6.x",
49
+ "swr": "2.x"
50
+ },
51
+ "devDependencies": {
52
+ "webpack": "^5.74.0"
53
+ },
54
+ "stableVersion": "4.0.0"
55
+ }
@@ -0,0 +1,51 @@
1
+ import { Type } from '@openmrs/esm-framework';
2
+
3
+ export const configSchema = {
4
+ radiologyConceptSetUuid: {
5
+ _type: Type.String,
6
+ _description: 'Radiology Concept SET UUID',
7
+ _default: '164068AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA',
8
+ },
9
+ radiologyConceptClassUuid: {
10
+ _type: Type.String,
11
+ _description: 'Radiology Concept Class UUID',
12
+ _default: '8caa332c-efe4-4025-8b18-3398328e1323',
13
+ },
14
+ orders: {
15
+ radiologyOrderTypeUuid: {
16
+ _type: Type.UUID,
17
+ _description: "UUID for the 'Radiology' order type",
18
+ _default: 'b4a7c280-369e-4d12-9ce8-18e36783fed6',
19
+ },
20
+ labOrderTypeUuid: {
21
+ _type: Type.UUID,
22
+ _description: "UUID for the 'Lab' order type",
23
+ _default: '52a447d3-a64a-11e3-9aeb-50e549534c5e',
24
+ },
25
+ labOrderableConcepts: {
26
+ _type: Type.Array,
27
+ _description:
28
+ 'UUIDs of concepts that represent orderable lab tests or lab sets. If an empty array `[]` is provided, every concept with class `Test` will be considered orderable.',
29
+ _elements: {
30
+ _type: Type.UUID,
31
+ },
32
+ _default: [],
33
+ },
34
+ },
35
+ };
36
+
37
+ interface OrderReason {
38
+ labTestUuid: string;
39
+ required: boolean;
40
+ orderReasons: Array<string>;
41
+ }
42
+ export type ImagingConfig = {
43
+ radiologyConceptSetUuid: string;
44
+ orders: {
45
+ labOrderTypeUuid: string;
46
+ labOrderableConcepts: Array<string>;
47
+ radiologyOrderTypeUuid: string;
48
+ };
49
+ labTestsWithOrderReasons: Array<OrderReason>;
50
+ radiologyConceptClassUuid: string;
51
+ };
@@ -0,0 +1,3 @@
1
+ export const moduleName = '@kenyaemr/esm-imaging-orders-app';
2
+ // TODO: Move this to config
3
+ export const BLEEDING_SITE = '162668AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA';
@@ -0,0 +1,6 @@
1
+ declare module '@carbon/react';
2
+ declare module '*.css';
3
+ declare module '*.scss';
4
+ declare module '*.png';
5
+
6
+ declare type SideNavProps = object;
@@ -0,0 +1,44 @@
1
+ @use '@carbon/layout';
2
+ @use '@carbon/colors';
3
+ @use '@carbon/type';
4
+
5
+ .container {
6
+ position: relative;
7
+ min-height: var(--desktop-workspace-window-height);
8
+ background-color: colors.$white;
9
+ display: flex;
10
+ flex-direction: column;
11
+ }
12
+
13
+ .patientHeader {
14
+ padding: layout.$spacing-03 layout.$spacing-05 layout.$spacing-06;
15
+ background-color: colors.$gray-10;
16
+
17
+ span:first-of-type {
18
+ margin-right: layout.$spacing-03;
19
+ }
20
+
21
+ .text02 {
22
+ color: colors.$gray-70;
23
+ }
24
+ }
25
+
26
+ .backButton {
27
+ padding: layout.$spacing-03;
28
+
29
+ button {
30
+ display: flex;
31
+ padding-left: 0 !important;
32
+ margin: 0 layout.$spacing-05;
33
+
34
+ svg {
35
+ order: 1;
36
+ margin-right: layout.$spacing-03;
37
+ margin-left: 0 !important;
38
+ }
39
+
40
+ span {
41
+ order: 2;
42
+ }
43
+ }
44
+ }
@@ -0,0 +1,86 @@
1
+ import React, { useCallback, useState } from 'react';
2
+ import classNames from 'classnames';
3
+ import capitalize from 'lodash-es/capitalize';
4
+ import { useTranslation } from 'react-i18next';
5
+ import { Button } from '@carbon/react';
6
+ import { ArrowLeft } from '@carbon/react/icons';
7
+ import {
8
+ DefaultWorkspaceProps,
9
+ age,
10
+ formatDate,
11
+ getPatientName,
12
+ parseDate,
13
+ useLayoutType,
14
+ usePatient,
15
+ } from '@openmrs/esm-framework';
16
+ import { launchPatientWorkspace } from '@openmrs/esm-patient-common-lib';
17
+ import { TestTypeSearch } from './imaging-type-search';
18
+ import { ImagingOrderForm } from './imaging-order-form.component';
19
+ import styles from './add-imaging-order.scss';
20
+ import { type ImagingOrderBasketItem } from '../../../types';
21
+
22
+ export interface AddImagingOrderWorkspaceAdditionalProps {
23
+ order?: ImagingOrderBasketItem;
24
+ }
25
+
26
+ export interface AddImagingOrderWorkspace extends DefaultWorkspaceProps, AddImagingOrderWorkspaceAdditionalProps {}
27
+
28
+ export default function AddImagingOrderWorkspace({
29
+ order: initialOrder,
30
+ closeWorkspace,
31
+ closeWorkspaceWithSavedChanges,
32
+ promptBeforeClosing,
33
+ }: AddImagingOrderWorkspace) {
34
+ const { t } = useTranslation();
35
+ const isTablet = useLayoutType() === 'tablet';
36
+ const { patient, isLoading: isLoadingPatient } = usePatient();
37
+ const [currentLabOrder, setCurrentLabOrder] = useState(initialOrder as ImagingOrderBasketItem);
38
+
39
+ const cancelOrder = useCallback(() => {
40
+ closeWorkspace({
41
+ ignoreChanges: true,
42
+ onWorkspaceClose: () => launchPatientWorkspace('order-basket'),
43
+ });
44
+ }, [closeWorkspace]);
45
+
46
+ return (
47
+ <div className={styles.container}>
48
+ {isTablet && !isLoadingPatient && (
49
+ <div className={styles.patientHeader}>
50
+ <span className={styles.bodyShort02}>{patient ? getPatientName(patient) : '--'}</span>
51
+ <span className={classNames(styles.text02, styles.bodyShort01)}>
52
+ {capitalize(patient?.gender)} &middot; {age(patient?.birthDate)} &middot;{' '}
53
+ <span>
54
+ {formatDate(parseDate(patient?.birthDate), {
55
+ mode: 'wide',
56
+ time: false,
57
+ })}
58
+ </span>
59
+ </span>
60
+ </div>
61
+ )}
62
+ {!isTablet && (
63
+ <div className={styles.backButton}>
64
+ <Button
65
+ kind="ghost"
66
+ renderIcon={(props) => <ArrowLeft size={24} {...props} />}
67
+ iconDescription="Return to order basket"
68
+ size="sm"
69
+ onClick={cancelOrder}>
70
+ <span>{t('backToOrderBasket', 'Back to order basket')}</span>
71
+ </Button>
72
+ </div>
73
+ )}
74
+ {!currentLabOrder ? (
75
+ <TestTypeSearch openLabForm={setCurrentLabOrder} />
76
+ ) : (
77
+ <ImagingOrderForm
78
+ initialOrder={currentLabOrder}
79
+ closeWorkspace={closeWorkspace}
80
+ closeWorkspaceWithSavedChanges={closeWorkspaceWithSavedChanges}
81
+ promptBeforeClosing={promptBeforeClosing}
82
+ />
83
+ )}
84
+ </div>
85
+ );
86
+ }
@@ -0,0 +1,354 @@
1
+ import React, { useCallback, useEffect, useState } from 'react';
2
+ import classNames from 'classnames';
3
+ import { launchPatientWorkspace, useOrderBasket } from '@openmrs/esm-patient-common-lib';
4
+ import { translateFrom, useLayoutType, useSession, DefaultWorkspaceProps, ExtensionSlot } from '@openmrs/esm-framework';
5
+ import { careSettingUuid, prepImagingOrderPostData, useConceptById } from '../api';
6
+ import {
7
+ Button,
8
+ ButtonSet,
9
+ Column,
10
+ ComboBox,
11
+ DatePicker,
12
+ DatePickerInput,
13
+ Form,
14
+ Layer,
15
+ Grid,
16
+ InlineNotification,
17
+ TextArea,
18
+ } from '@carbon/react';
19
+ import { useTranslation } from 'react-i18next';
20
+ import { priorityOptions } from './imaging-order';
21
+ import { useImagingTypes } from './useImagingTypes';
22
+ import { Controller, type FieldErrors, useForm } from 'react-hook-form';
23
+ import { zodResolver } from '@hookform/resolvers/zod';
24
+ import { z } from 'zod';
25
+ import { moduleName, BLEEDING_SITE } from '../../../constants';
26
+ import styles from './imaging-order-form.scss';
27
+ import type { ImagingOrderBasketItem } from '../../../types';
28
+
29
+ export interface ImagingOrderFormProps {
30
+ initialOrder: ImagingOrderBasketItem;
31
+ closeWorkspace: DefaultWorkspaceProps['closeWorkspace'];
32
+ closeWorkspaceWithSavedChanges: DefaultWorkspaceProps['closeWorkspaceWithSavedChanges'];
33
+ promptBeforeClosing: DefaultWorkspaceProps['promptBeforeClosing'];
34
+ }
35
+
36
+ // Designs:
37
+ // https://app.zeplin.io/project/60d5947dd636aebbd63dce4c/screen/640b06c440ee3f7af8747620
38
+ // https://app.zeplin.io/project/60d5947dd636aebbd63dce4c/screen/640b06d286e0aa7b0316db4a
39
+ export function ImagingOrderForm({
40
+ initialOrder,
41
+ closeWorkspace,
42
+ closeWorkspaceWithSavedChanges,
43
+ promptBeforeClosing,
44
+ }: ImagingOrderFormProps) {
45
+ const { t } = useTranslation();
46
+ const isTablet = useLayoutType() === 'tablet';
47
+ const session = useSession();
48
+ const { orders, setOrders } = useOrderBasket<ImagingOrderBasketItem>('imaging', prepImagingOrderPostData);
49
+ const { testTypes, isLoading: isLoadingTestTypes, error: errorLoadingTestTypes } = useImagingTypes();
50
+ const [showErrorNotification, setShowErrorNotification] = useState(false);
51
+
52
+ const lateralityItems = [
53
+ { value: 'LEFT', label: 'Left' },
54
+ { value: 'RIGHT', label: 'Right' },
55
+ { value: 'BILATERAL', label: 'Bilateral' },
56
+ ];
57
+ const {
58
+ items: { answers: bodySiteItems = [] },
59
+ } = useConceptById(BLEEDING_SITE);
60
+
61
+ const imagingOrderFormSchema = z.object({
62
+ instructions: z.string().optional(),
63
+ urgency: z.string().refine((value) => value !== '', {
64
+ message: translateFrom(moduleName, 'addLabOrderPriorityRequired', 'Priority is required'),
65
+ }),
66
+ testType: z.object(
67
+ { label: z.string(), conceptUuid: z.string() },
68
+ {
69
+ required_error: translateFrom(moduleName, 'addLabOrderLabTestTypeRequired', 'Test type is required'),
70
+ invalid_type_error: translateFrom(moduleName, 'addLabOrderLabReferenceRequired', 'Test type is required'),
71
+ },
72
+ ),
73
+ scheduleDate: z.union([z.string(), z.date(), z.string().optional()]),
74
+ commentsToFulfiller: z.string().optional(),
75
+ laterality: z.string().optional(),
76
+ bodySite: z.string().optional(),
77
+ });
78
+
79
+ const {
80
+ control,
81
+ handleSubmit,
82
+ formState: { errors, defaultValues, isDirty },
83
+ } = useForm<ImagingOrderBasketItem>({
84
+ mode: 'all',
85
+ resolver: zodResolver(imagingOrderFormSchema),
86
+ defaultValues: {
87
+ ...initialOrder,
88
+ },
89
+ });
90
+
91
+ const handleFormSubmission = useCallback(
92
+ (data: ImagingOrderBasketItem) => {
93
+ data.action = 'NEW';
94
+ data.careSetting = careSettingUuid;
95
+ data.orderer = session.currentProvider.uuid;
96
+ const newOrders = [...orders];
97
+ const existingOrder = orders.find((order) => order.testType.conceptUuid == defaultValues.testType.conceptUuid);
98
+ const orderIndex = existingOrder ? orders.indexOf(existingOrder) : orders.length;
99
+ newOrders[orderIndex] = data;
100
+ setOrders(newOrders);
101
+ closeWorkspaceWithSavedChanges({
102
+ onWorkspaceClose: () => launchPatientWorkspace('order-basket'),
103
+ });
104
+ },
105
+ [orders, setOrders, defaultValues, closeWorkspaceWithSavedChanges, session],
106
+ );
107
+
108
+ const cancelOrder = useCallback(() => {
109
+ setOrders(orders.filter((order) => order.testType.conceptUuid !== defaultValues.testType.conceptUuid));
110
+ closeWorkspace({
111
+ onWorkspaceClose: () => launchPatientWorkspace('order-basket'),
112
+ });
113
+ }, [closeWorkspace, orders, setOrders, defaultValues]);
114
+
115
+ const onError = (errors: FieldErrors<ImagingOrderBasketItem>) => {
116
+ if (errors) {
117
+ setShowErrorNotification(true);
118
+ }
119
+ };
120
+
121
+ useEffect(() => {
122
+ promptBeforeClosing(() => isDirty);
123
+ }, [isDirty, promptBeforeClosing]);
124
+
125
+ const [showScheduleDate, setShowScheduleDate] = useState(false);
126
+
127
+ return (
128
+ <>
129
+ {errorLoadingTestTypes && (
130
+ <InlineNotification
131
+ kind="error"
132
+ lowContrast
133
+ className={styles.inlineNotification}
134
+ title={t('errorLoadingTestTypes', 'Error occurred when loading test types')}
135
+ subtitle={t('tryReopeningTheForm', 'Please try launching the form again')}
136
+ />
137
+ )}
138
+ <Form className={styles.orderForm} onSubmit={handleSubmit(handleFormSubmission, onError)}>
139
+ <div className={styles.form}>
140
+ <ExtensionSlot name="top-of-imaging-order-form-slot" state={{ order: initialOrder }} />
141
+
142
+ <Grid className={styles.gridRow}>
143
+ <Column lg={16} md={8} sm={4}>
144
+ <InputWrapper>
145
+ <Controller
146
+ name="testType"
147
+ control={control}
148
+ render={({ field: { onChange, onBlur, value } }) => (
149
+ <ComboBox
150
+ size="lg"
151
+ id="testTypeInput"
152
+ titleText={t('testType', 'Test type')}
153
+ selectedItem={value}
154
+ items={testTypes ?? []}
155
+ placeholder={
156
+ isLoadingTestTypes ? `${t('loading', 'Loading')}...` : t('testTypePlaceholder', 'Select one')
157
+ }
158
+ onBlur={onBlur}
159
+ disabled={isLoadingTestTypes}
160
+ onChange={({ selectedItem }) => onChange(selectedItem)}
161
+ invalid={errors.testType?.message}
162
+ invalidText={errors.testType?.message}
163
+ />
164
+ )}
165
+ />
166
+ </InputWrapper>
167
+ </Column>
168
+ </Grid>
169
+ <Grid className={styles.gridRow}>
170
+ <Column lg={8} md={8} sm={4}>
171
+ <InputWrapper>
172
+ <Controller
173
+ name="urgency"
174
+ control={control}
175
+ render={({ field: { onChange, onBlur, value } }) => (
176
+ <ComboBox
177
+ size="lg"
178
+ id="priorityInput"
179
+ titleText={t('priority', 'Priority')}
180
+ selectedItem={priorityOptions.find((option) => option.value === value) || null}
181
+ items={priorityOptions}
182
+ onBlur={onBlur}
183
+ onChange={({ selectedItem }) => {
184
+ onChange(selectedItem?.value || '');
185
+ setShowScheduleDate(selectedItem?.label === 'Scheduled');
186
+ }}
187
+ invalid={errors.urgency?.message}
188
+ invalidText={errors.urgency?.message}
189
+ />
190
+ )}
191
+ />
192
+ </InputWrapper>
193
+ </Column>
194
+ </Grid>
195
+ {showScheduleDate && (
196
+ <Grid className={styles.gridRow}>
197
+ <Column lg={16} md={4} sm={4}>
198
+ <div className={styles.fullWidthDatePickerContainer}>
199
+ <InputWrapper>
200
+ <Controller
201
+ name="scheduleDate"
202
+ control={control}
203
+ render={({ field: { onBlur, value, onChange, ref } }) => (
204
+ <DatePicker
205
+ datePickerType="single"
206
+ value={value}
207
+ onChange={([newStartDate]) => onChange(newStartDate)}
208
+ onBlur={onBlur}
209
+ ref={ref}>
210
+ <DatePickerInput
211
+ id="scheduleDatePicker"
212
+ placeholder="mm/dd/yyyy"
213
+ labelText={t('scheduleDate', 'Scheduled date')}
214
+ size="lg"
215
+ />
216
+ </DatePicker>
217
+ )}
218
+ />
219
+ </InputWrapper>
220
+ </div>
221
+ </Column>
222
+ </Grid>
223
+ )}
224
+ <Grid className={styles.gridRow}>
225
+ <Column lg={16} md={8} sm={4}>
226
+ <InputWrapper>
227
+ <Controller
228
+ name="laterality"
229
+ control={control}
230
+ render={({ field: { onChange, onBlur, value } }) => (
231
+ <ComboBox
232
+ size="lg"
233
+ id="lateralityInput"
234
+ titleText={t('laterality', 'Laterality')}
235
+ selectedItem={lateralityItems?.find((option) => option.value === value) || null}
236
+ items={lateralityItems}
237
+ onBlur={onBlur}
238
+ onChange={({ selectedItem }) => onChange(selectedItem?.value || '')}
239
+ invalid={errors.laterality?.message}
240
+ invalidText={errors.laterality?.message}
241
+ itemToString={(item) => item?.label}
242
+ />
243
+ )}
244
+ />
245
+ </InputWrapper>
246
+ </Column>
247
+ </Grid>
248
+ <Grid className={styles.gridRow}>
249
+ <Column lg={16} md={8} sm={4}>
250
+ <InputWrapper>
251
+ <Controller
252
+ name="bodySite"
253
+ control={control}
254
+ render={({ field: { onChange, onBlur, value } }) => (
255
+ <ComboBox
256
+ size="lg"
257
+ id="bodySiteInput"
258
+ titleText={t('bodySite', 'Body Site')}
259
+ selectedItem={bodySiteItems?.find((option) => option.uuid === value) || null}
260
+ items={bodySiteItems}
261
+ onBlur={onBlur}
262
+ onChange={({ selectedItem }) => onChange(selectedItem?.uuid || '')}
263
+ invalid={errors.bodySite?.message}
264
+ invalidText={errors.bodySite?.message}
265
+ itemToString={(item) => item?.display}
266
+ />
267
+ )}
268
+ />
269
+ </InputWrapper>
270
+ </Column>
271
+ </Grid>
272
+ <Grid className={styles.gridRow}>
273
+ <Column lg={16} md={8} sm={4}>
274
+ <InputWrapper>
275
+ <Controller
276
+ name="instructions"
277
+ control={control}
278
+ render={({ field: { onChange, onBlur, value } }) => (
279
+ <TextArea
280
+ enableCounter
281
+ id="additionalInstructionsInput"
282
+ size="lg"
283
+ labelText={t('additionalInstructions', 'Additional instructions')}
284
+ value={value}
285
+ onChange={onChange}
286
+ onBlur={onBlur}
287
+ maxCount={500}
288
+ invalid={errors.instructions?.message}
289
+ invalidText={errors.instructions?.message}
290
+ />
291
+ )}
292
+ />
293
+ </InputWrapper>
294
+ </Column>
295
+ </Grid>
296
+ <Grid className={styles.gridRow}>
297
+ <Column lg={16} md={8} sm={4}>
298
+ <InputWrapper>
299
+ <Controller
300
+ name="commentsToFulfiller"
301
+ control={control}
302
+ render={({ field: { onChange, onBlur, value } }) => (
303
+ <TextArea
304
+ enableCounter
305
+ id="commentsToFulfillerInput"
306
+ size="lg"
307
+ labelText={t('commentsToFulfiller', 'Comments To Fulfiller')}
308
+ value={value}
309
+ onChange={onChange}
310
+ onBlur={onBlur}
311
+ maxCount={500}
312
+ invalid={errors.commentsToFulfiller?.message}
313
+ invalidText={errors.commentsToFulfiller?.message}
314
+ />
315
+ )}
316
+ />
317
+ </InputWrapper>
318
+ </Column>
319
+ </Grid>
320
+ </div>
321
+ <div>
322
+ {showErrorNotification && (
323
+ <Column className={styles.errorContainer}>
324
+ <InlineNotification
325
+ lowContrast
326
+ title={t('error', 'Error')}
327
+ subtitle={t('pleaseRequiredFields', 'Please fill all required fields') + '.'}
328
+ onClose={() => setShowErrorNotification(false)}
329
+ />
330
+ </Column>
331
+ )}
332
+ <ButtonSet
333
+ className={classNames(styles.buttonSet, isTablet ? styles.tabletButtonSet : styles.desktopButtonSet)}>
334
+ <Button className={styles.button} kind="secondary" onClick={cancelOrder} size="xl">
335
+ {t('discard', 'Discard')}
336
+ </Button>
337
+ <Button className={styles.button} kind="primary" type="submit" size="xl">
338
+ {t('saveOrder', 'Save order')}
339
+ </Button>
340
+ </ButtonSet>
341
+ </div>
342
+ </Form>
343
+ </>
344
+ );
345
+ }
346
+
347
+ function InputWrapper({ children }) {
348
+ const isTablet = useLayoutType() === 'tablet';
349
+ return (
350
+ <Layer level={isTablet ? 1 : 0}>
351
+ <div className={styles.field}>{children}</div>
352
+ </Layer>
353
+ );
354
+ }