diff --git a/.github/workflows/node.js.yml b/.github/workflows/node.js.yml index 9fc4b2c..3686969 100644 --- a/.github/workflows/node.js.yml +++ b/.github/workflows/node.js.yml @@ -11,7 +11,7 @@ on: env: ESM_NAME: "@openmrs/esm-template-app" - JS_NAME: "openmrs-esm-template-app.js" + JS_NAME: "openmrs-esm-patient-medical-supply-orders-app.js" jobs: build: diff --git a/README.md b/README.md index f0d7581..c414902 100644 --- a/README.md +++ b/README.md @@ -1,14 +1,8 @@ -![Node.js CI](https://github.com/openmrs/openmrs-esm-template-app/workflows/Node.js%20CI/badge.svg) +![Node.js CI](https://github.com/openmrs/openmrs-esm-patient-medical-supply-orders-app/workflows/Node.js%20CI/badge.svg) -# OpenMRS ESM Template App +# Patient Medical Supply Orders App -This repository provides a starting point for creating your own -[OpenMRS Microfrontend](https://wiki.openmrs.org/display/projects/OpenMRS+3.0%3A+A+Frontend+Framework+that+enables+collaboration+and+better+User+Experience). - -For more information, please see the -[OpenMRS Frontend Developer Documentation](https://o3-docs.openmrs.org/#/). - -In particular, the [Setup](https://o3-docs.openmrs.org/docs/frontend-modules/setup) section can help you get started developing microfrontends in general. The [Creating a microfrontend](https://o3-docs.openmrs.org/docs/recipes/create-a-frontend-module) section provides information about how to use this repository to create your own microfrontend. +This repository adds a new Patient Medical Supply order type to the Patient Chart Order Basket. The users will be able to place Medical Supply Orders and define the quantity and the Quantity Units for the order placed. ## Running this code @@ -18,28 +12,7 @@ yarn start # to run the dev server ``` Once it is running, a browser window -should open with the OpenMRS 3 application. Log in and then navigate to `/openmrs/spa/root`. - -## Adapting the code - -1. Start by finding and replacing all instances of "template" with the name - of your microfrontend. -2. Update `index.ts` as appropriate, at least changing the feature name and the page name and route. -3. Rename the `root.*` family of files to have the name of your first page. -4. Delete the contents of the objects in `config-schema`. Start filling them back in once you have a clear idea what will need to be configured. -5. Delete the `greeter` and `patient-getter` directories, and the contents of `root.component.tsx`. -6. Delete the contents of `translations/en.json`. -7. Open up `.github/workflows` and adapt it to your needs. If you're writing - a microfrontend that will be managed by the community, you might be able to - just replace all instances of `template` with your microfrontend's name. - However, if you're writing a microfrontend for a specific organization or - implementation, you will probably need to configure GitHub Actions differently. -8. Delete the contents of this README and write a short explanation of what - you intend to build. Links to planning or design documents can be very helpful. - -At this point, you should be able to write your first page as a React application. - -Check out the [Medication dispensing app](https://github.com/openmrs/openmrs-esm-dispensing-app) for an example of a non-trivial app built using the Template. +should open with the OpenMRS 3 application. ## Integrating it into your application diff --git a/package.json b/package.json index dfe321f..e570155 100644 --- a/package.json +++ b/package.json @@ -1,9 +1,9 @@ { - "name": "@openmrs/esm-template-app", + "name": "@openmrs/esm-patient-medical-supply-orders-app", "version": "4.0.0", "license": "MPL-2.0", "description": "An OpenMRS seed application for building microfrontends", - "browser": "dist/openmrs-esm-template-app.js", + "browser": "dist/openmrs-esm-patient-medical-supply-orders-app.js", "main": "src/index.ts", "source": true, "scripts": { @@ -30,29 +30,36 @@ ], "repository": { "type": "git", - "url": "git+https://github.com/openmrs/openmrs-esm-template-app.git" + "url": "git+https://github.com/openmrs/openmrsesm-patient-medical-supply-orders-app.git" }, - "homepage": "https://github.com/openmrs/openmrs-esm-template-app#readme", + "homepage": "https://github.com/openmrs/openmrsesm-patient-medical-supply-orders-app#readme", "publishConfig": { "access": "public" }, "bugs": { - "url": "https://github.com/openmrs/openmrs-esm-template-app/issues" + "url": "https://github.com/openmrs/openmrsesm-patient-medical-supply-orders-app/issues" }, "dependencies": { - "@carbon/react": "^1.68.0", - "lodash-es": "^4.17.21" + "@hookform/resolvers": "^3.9.1", + "lodash-es": "^4.17.21", + "react-hook-form": "^7.53.2", + "zod": "^3.23.8" }, "peerDependencies": { - "@openmrs/esm-framework": "*", + "@carbon/react": "^1.x", + "@openmrs/esm-framework": "5.x", + "@openmrs/esm-patient-common-lib": "8.x", "dayjs": "1.x", "react": "18.x", "react-i18next": "11.x", "react-router-dom": "6.x", - "rxjs": "6.x" + "rxjs": "6.x", + "swr": "2.x" }, "devDependencies": { + "@carbon/react": "^1.71.0", "@openmrs/esm-framework": "next", + "@openmrs/esm-patient-common-lib": "next", "@openmrs/esm-styleguide": "next", "@playwright/test": "^1.42.1", "@swc/cli": "^0.3.12", @@ -93,6 +100,7 @@ "react-router-dom": "^6.14.1", "rxjs": "^6.6.7", "swc-loader": "^0.2.3", + "swr": "^2.2.5", "turbo": "^2.2.3", "typescript": "^4.9.5", "webpack": "^5.88.1", @@ -102,5 +110,8 @@ "packages/**/src/**/*.{ts,tsx}": "eslint --cache --fix --max-warnings 0", "*.{css,scss,ts,tsx}": "prettier --write --list-different" }, - "packageManager": "yarn@4.3.1" + "packageManager": "yarn@4.3.1", + "resolutions": { + "@openmrs/esm-patient-common-lib": "portal:/Users/vasharma05/Projects/patient-chart/packages/esm-patient-common-lib" + } } diff --git a/src/boxes/extensions/blue-box.component.tsx b/src/boxes/extensions/blue-box.component.tsx deleted file mode 100644 index 8fc2150..0000000 --- a/src/boxes/extensions/blue-box.component.tsx +++ /dev/null @@ -1,15 +0,0 @@ -/** - * This component demonstrates the creation of an extension. - * - * Check out the Extension System docs: - * https://o3-docs.vercel.app/docs/extension-system - */ - -import React from 'react'; -import styles from './box.scss'; - -const BlueBox: React.FC = () => { - return
; -}; - -export default BlueBox; diff --git a/src/boxes/extensions/box.scss b/src/boxes/extensions/box.scss deleted file mode 100644 index 740603d..0000000 --- a/src/boxes/extensions/box.scss +++ /dev/null @@ -1,23 +0,0 @@ -/* Extensions should supply the minimum amount of styling - * necessary. Here, the extensions set only their colors. - * Their sizes and other general features of their display - * is controlled by the slot. */ -@use "@carbon/layout"; -@use '@openmrs/esm-styleguide/src/vars' as *; - - .blue { - background-color: darkblue; -} - -.red { - background-color: darkred; -} - -/* Brand colors are special. They must be included using a - * SASS mix-in (shown here) or using a CSS variable like - * `var(--brand-01)`. */ -.brand { - @include brand-01(background-color); - color: white; - padding: layout.$spacing-03; -} diff --git a/src/boxes/extensions/brand-box.component.tsx b/src/boxes/extensions/brand-box.component.tsx deleted file mode 100644 index 2389aa9..0000000 --- a/src/boxes/extensions/brand-box.component.tsx +++ /dev/null @@ -1,15 +0,0 @@ -/** - * This component demonstrates the creation of an extension. - * - * Check out the Extension System docs: - * https://o3-docs.vercel.app/docs/extension-system - */ - -import React from 'react'; -import styles from './box.scss'; - -const RedBox: React.FC = () => { - return
; -}; - -export default RedBox; diff --git a/src/boxes/extensions/red-box.component.tsx b/src/boxes/extensions/red-box.component.tsx deleted file mode 100644 index 0219b70..0000000 --- a/src/boxes/extensions/red-box.component.tsx +++ /dev/null @@ -1,15 +0,0 @@ -/** - * This component demonstrates the creation of an extension. - * - * Check out the Extension System docs: - * https://o3-docs.vercel.app/docs/extension-system - */ - -import React from 'react'; -import styles from './box.scss'; - -const RedBox: React.FC = () => { - return
; -}; - -export default RedBox; diff --git a/src/boxes/slot/boxes.component.tsx b/src/boxes/slot/boxes.component.tsx deleted file mode 100644 index 3d516fc..0000000 --- a/src/boxes/slot/boxes.component.tsx +++ /dev/null @@ -1,25 +0,0 @@ -import React from 'react'; -import { useTranslation } from 'react-i18next'; -import { Extension, ExtensionSlot } from '@openmrs/esm-framework'; -import styles from './boxes.scss'; - -export const Boxes: React.FC = () => { - const { t } = useTranslation(); - - return ( -
-
{t('extensionSystem', 'Extension system')}
-

- {t( - 'extensionExplainer', - 'Here are some colored boxes. Because they are attached as extensions within a slot, an admin can change what boxes are shown using configuration. These boxes happen to be defined in this module, but they could attach to this slot even if they were in a different module.', - )} -

- -
- -
-
-
- ); -}; diff --git a/src/boxes/slot/boxes.scss b/src/boxes/slot/boxes.scss deleted file mode 100644 index 0414982..0000000 --- a/src/boxes/slot/boxes.scss +++ /dev/null @@ -1,29 +0,0 @@ -/* General features of extension styling are controlled - * at the slot level. The boxes should know as little as - * possible about the context where they are used, so - * they do not set their own sizes. - * - * `> * > *` is used to target the outermost DOM node of - * the extension. - */ - @use '@carbon/layout'; - -.box > * > * { - height: layout.$spacing-10; - width: layout.$spacing-10; -} - -.boxes { - margin: layout.$spacing-07 0; - display: flex; - gap: layout.$spacing-05; -} - -.container { - max-width: 50rem; - margin-top: layout.$spacing-09; - - > * + * { - margin-top: layout.$spacing-06; - } -} diff --git a/src/config-schema.ts b/src/config-schema.ts index d07a785..9b3df4a 100644 --- a/src/config-schema.ts +++ b/src/config-schema.ts @@ -1,43 +1,72 @@ -import { Type, validator } from '@openmrs/esm-framework'; +import { Type, validator, validators } from '@openmrs/esm-framework'; +import _default from 'react-hook-form/dist/utils/createSubject'; -/** - * This is the config schema. It expects a configuration object which - * looks like this: - * - * ```json - * { "casualGreeting": true, "whoToGreet": ["Mom"] } - * ``` - * - * In OpenMRS Microfrontends, all config parameters are optional. Thus, - * all elements must have a reasonable default. A good default is one - * that works well with the reference application. - * - * To understand the schema below, please read the configuration system - * documentation: - * https://openmrs.github.io/openmrs-esm-core/#/main/config - * Note especially the section "How do I make my module configurable?" - * https://openmrs.github.io/openmrs-esm-core/#/main/config?id=im-developing-an-esm-module-how-do-i-make-it-configurable - * and the Schema Reference - * https://openmrs.github.io/openmrs-esm-core/#/main/config?id=schema-reference - */ export const configSchema = { - casualGreeting: { - _type: Type.Boolean, - _default: false, - _description: 'Whether to use a casual greeting (or a formal one).', - }, - whoToGreet: { + orderTypes: { _type: Type.Array, - _default: ['World'], - _description: 'Who should be greeted. Names will be separated by a comma and space.', _elements: { - _type: Type.String, + _type: Type.Object, + orderTypeUuid: { + _type: Type.UUID, + _description: 'The UUID of the order type with the listed in the order basket', + }, + orderableConceptClasses: { + _type: Type.Array, + _description: + 'The concept class of the orderable concepts. By default it will look for concept class in the order type response', + _elements: { + _type: Type.UUID, + }, + }, + orderableConceptSets: { + _type: Type.Array, + _description: + "UUIDs of concepts that represent orderable concepts. Either the `conceptClass` should be given, or the `orderableConcepts`. If the orderableConcepts are not given, then it'll search concepts by concept class.", + _elements: { + _type: Type.UUID, + }, + }, + }, + _default: [ + { + orderTypeUuid: '67a92bd6-0f88-11ea-8d71-362b9e155667', + orderableConceptClasses: [], + orderableConceptSets: [], + }, + ], + }, + quantityUnits: { + _type: Type.Object, + _description: 'Concept to be used for fetching quantity units', + _default: { + conceptUuid: '162402AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', + map: 'setMembers', + }, + // _elements: { + conceptUuid: { + _type: Type.UUID, + _description: 'UUID for the quantity units concepts', + _default: '162402AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', + }, + map: { + _type: Type.UUID, + _description: + "Whether to use the concept answers of the setMembers of the concept. One of 'answers' or 'setMembers'.", + _default: 'setMembers', + _validators: [validators.oneOf(['answers', 'setMembers'])], }, - _validators: [validator((v) => v.length > 0, 'At least one person must be greeted.')], }, + // }, }; -export type Config = { - casualGreeting: boolean; - whoToGreet: Array; +export type ConfigObject = { + orderTypes: Array<{ + orderTypeUuid: string; + orderableConceptClasses: Array; + orderableConceptSets: Array; + }>; + quantityUnits: { + conceptUuid: string; + map: 'answers' | 'setMembers'; + }; }; diff --git a/src/constants.ts b/src/constants.ts new file mode 100644 index 0000000..8a8d205 --- /dev/null +++ b/src/constants.ts @@ -0,0 +1 @@ +export const moduleName = '@openmrs/esm-patient-medical-supply-orders-app'; diff --git a/src/greeter/greeter.component.tsx b/src/greeter/greeter.component.tsx deleted file mode 100644 index e2ecc20..0000000 --- a/src/greeter/greeter.component.tsx +++ /dev/null @@ -1,42 +0,0 @@ -/** - * This component demonstrates usage of the config object. Its structure - * comes from `../config-schema.ts`. For more information about the - * configuration system, read the docs: https://o3-docs.vercel.app/docs/configuration-system - */ -import React from 'react'; -import { Tile } from '@carbon/react'; -import { Trans, useTranslation } from 'react-i18next'; -import { useConfig } from '@openmrs/esm-framework'; -import { type Config } from '../config-schema'; -import styles from './greeter.scss'; - -const Greeter: React.FC = () => { - const { t } = useTranslation(); - const config: Config = useConfig(); - - return ( -
-
{t('configSystem', 'Configuration system')}
-

- - The greeting shown below is driven by the configuration system. To change the configuration properties, click - the spanner icon in the navbar to pull up the Implementer Tools panel. Then, type template into the{' '} - Search configuration input. This should filter the configuration properties to show only those that - are relevant to this module. You can change the values of these properties and click Save to see the - changes reflected in the UI - - . -

-
- - {config.casualGreeting ? hey : hello}{' '} - {/* t('world') */} - {config.whoToGreet.join(', ')}! - -
-
-
- ); -}; - -export default Greeter; diff --git a/src/greeter/greeter.scss b/src/greeter/greeter.scss deleted file mode 100644 index 14621e2..0000000 --- a/src/greeter/greeter.scss +++ /dev/null @@ -1,20 +0,0 @@ -@use '@carbon/layout'; -@use '@carbon/type'; - -.container { - max-width: 50rem; - - > * + * { - margin-top: layout.$spacing-05; - } -} - -.greeting { - text-transform: capitalize; -} - -.tile { - border: 1px solid lightgray; - max-width: 15rem; - @include type.type-style('heading-compact-01'); -} diff --git a/src/greeter/greeter.test.tsx b/src/greeter/greeter.test.tsx deleted file mode 100644 index c71ace6..0000000 --- a/src/greeter/greeter.test.tsx +++ /dev/null @@ -1,28 +0,0 @@ -import React from 'react'; -import { render, screen } from '@testing-library/react'; -import { useConfig } from '@openmrs/esm-framework'; -import { Config } from '../config-schema'; -import Greeter from './greeter.component'; - -const mockUseConfig = jest.mocked(useConfig); - -it('displays the expected default text', () => { - const config: Config = { casualGreeting: false, whoToGreet: ['World'] }; - mockUseConfig.mockReturnValue(config); - - render(); - - expect(screen.getByText(/world/i)).toHaveTextContent('hello World!'); -}); - -it('casually greets my friends', () => { - const config: Config = { - casualGreeting: true, - whoToGreet: ['Ariel', 'Barak', 'Callum'], - }; - mockUseConfig.mockReturnValue(config); - - render(); - - expect(screen.getByText(/ariel/i)).toHaveTextContent('hey Ariel, Barak, Callum!'); -}); diff --git a/src/index.ts b/src/index.ts index f889251..ccfcfba 100644 --- a/src/index.ts +++ b/src/index.ts @@ -6,44 +6,39 @@ */ import { getAsyncLifecycle, defineConfigSchema } from '@openmrs/esm-framework'; import { configSchema } from './config-schema'; - -const moduleName = '@openmrs/esm-template-app'; +import { moduleName } from './constants'; const options = { - featureName: 'root-world', + featureName: 'patient-medical-supply-order', moduleName, }; -/** - * This tells the app shell how to obtain translation files: that they - * are JSON files in the directory `../translations` (which you should - * see in the directory structure). - */ export const importTranslation = require.context('../translations', false, /.json$/, 'lazy'); -/** - * This function performs any setup that should happen at microfrontend - * load-time (such as defining the config schema) and then returns an - * object which describes how the React application(s) should be - * rendered. - */ export function startupApp() { defineConfigSchema(moduleName, configSchema); } -/** - * This named export tells the app shell that the default export of `root.component.tsx` - * should be rendered when the route matches `root`. The full route - * will be `openmrsSpaBase() + 'root'`, which is usually - * `/openmrs/spa/root`. - */ -export const root = getAsyncLifecycle(() => import('./root.component'), options); - -/** - * The following are named exports for the extensions defined in this frontend modules. See the `routes.json` file to see how these are used. - */ -export const redBox = getAsyncLifecycle(() => import('./boxes/extensions/red-box.component'), options); - -export const blueBox = getAsyncLifecycle(() => import('./boxes/extensions/blue-box.component'), options); - -export const brandBox = getAsyncLifecycle(() => import('./boxes/extensions/brand-box.component'), options); +export const medicalSupplyOrderPanel = getAsyncLifecycle( + () => import('./medical-orders/medical-supply-order-type.component'), + options, +); + +// t('searchMedicalSupplyOrderables', 'Search medical supply orderables') +export const searchMedicalSupplyOrderables = getAsyncLifecycle( + () => + import( + './medical-orders/medical-supply-orderable-concept-search/medical-supply-orderable-concept-search.workspace' + ), + options, +); + +export const modifyMedicalSupplyOrderMenuItem = getAsyncLifecycle( + () => import('./medical-orders/action-menu-items/modify-medical-supply-order-menu-item.extension'), + options, +); + +export const medicalSupplyOrderDetailTable = getAsyncLifecycle( + () => import('./medical-orders/medical-supply-detail-table/medical-supply-detail.extension'), + options, +); diff --git a/src/medical-orders/action-menu-items/modify-medical-supply-order-menu-item.extension.tsx b/src/medical-orders/action-menu-items/modify-medical-supply-order-menu-item.extension.tsx new file mode 100644 index 0000000..c9a5dd7 --- /dev/null +++ b/src/medical-orders/action-menu-items/modify-medical-supply-order-menu-item.extension.tsx @@ -0,0 +1,73 @@ +import { OverflowMenuItem } from '@carbon/react'; +import { useLaunchWorkspaceRequiringVisit, useOrderBasket, type Order } from '@openmrs/esm-patient-common-lib'; +import React from 'react'; +import { type MedicalSupplyOrderBasketItem } from '../types'; +import { buildMedicalSupplyOrderItem } from '../resources'; +import { useTranslation } from 'react-i18next'; +import { OverflowMenu } from '@carbon/react'; +import { Layer } from '@carbon/react'; + +interface ModifyMedicalSupplyOrderMenuItemProps { + orderItem: Order; + className: string; + responsiveSize: string; +} + +export default function ModifyMedicalSupplyOrderMenuItem({ + className, + orderItem, + responsiveSize, +}: ModifyMedicalSupplyOrderMenuItemProps) { + const { t } = useTranslation(); + const openMedicalSupplyOrderFormWorkspace = useLaunchWorkspaceRequiringVisit( + 'medical-supply-orderable-concept-workspace', + ); + const launchOrderBasket = useLaunchWorkspaceRequiringVisit('order-basket'); + + const { orders, setOrders } = useOrderBasket(orderItem.orderType.uuid); + const alreadyInBasket = orders.some((x) => x.uuid === orderItem.uuid); + + const handleModifyOrder = () => { + const order = buildMedicalSupplyOrderItem(orderItem, 'REVISE'); + setOrders([...orders, order]); + openMedicalSupplyOrderFormWorkspace({ + orderTypeUuid: orderItem.orderType.uuid, + order, + }); + }; + + const handleCancelOrder = () => { + const order = buildMedicalSupplyOrderItem(orderItem, 'DISCONTINUE'); + setOrders([...orders, order]); + launchOrderBasket(); + }; + + return ( + + + + + + + ); +} diff --git a/src/medical-orders/medical-supply-detail-table/medical-supply-detail.extension.tsx b/src/medical-orders/medical-supply-detail-table/medical-supply-detail.extension.tsx new file mode 100644 index 0000000..e150e23 --- /dev/null +++ b/src/medical-orders/medical-supply-detail-table/medical-supply-detail.extension.tsx @@ -0,0 +1,28 @@ +import React, { useMemo } from 'react'; +import styles from './medical-supply-detail.scss'; +import { type Order } from '@openmrs/esm-patient-common-lib'; +import { useTranslation } from 'react-i18next'; +import { useLayoutType } from '@openmrs/esm-framework'; + +interface TestOrderProps { + orderItem: Order; +} + +const MedicalSupplyOrderDetailTable: React.FC = ({ orderItem }) => { + const { t } = useTranslation(); + return ( +
+
+ {t('quantity', 'Quantity')} {orderItem.quantity ?? 0}{' '} + {orderItem.quantityUnits?.display} +
+ {orderItem.instructions && ( +
+ {t('instructions', 'Instructions')} {orderItem.instructions} +
+ )} +
+ ); +}; + +export default MedicalSupplyOrderDetailTable; diff --git a/src/medical-orders/medical-supply-detail-table/medical-supply-detail.scss b/src/medical-orders/medical-supply-detail-table/medical-supply-detail.scss new file mode 100644 index 0000000..279f2d6 --- /dev/null +++ b/src/medical-orders/medical-supply-detail-table/medical-supply-detail.scss @@ -0,0 +1,11 @@ +@use '@carbon/layout'; +@use '@carbon/type'; +@use '@openmrs/esm-styleguide/src/vars' as *; + +.label01 { + @include type.type-style('label-01'); +} + +.order { + @include type.type-style('body-short-02'); +} \ No newline at end of file diff --git a/src/medical-orders/medical-supply-order-form/medical-supply-order-form.component.tsx b/src/medical-orders/medical-supply-order-form/medical-supply-order-form.component.tsx new file mode 100644 index 0000000..c0573de --- /dev/null +++ b/src/medical-orders/medical-supply-order-form/medical-supply-order-form.component.tsx @@ -0,0 +1,353 @@ +import React, { useCallback, useEffect, useMemo, useState } from 'react'; +import classNames from 'classnames'; +import { + type DefaultPatientWorkspaceProps, + launchPatientWorkspace, + useOrderBasket, + useOrderType, + priorityOptions, +} from '@openmrs/esm-patient-common-lib'; +import { translateFrom, useLayoutType, useSession, useConfig, ExtensionSlot } from '@openmrs/esm-framework'; +import { + Button, + ButtonSet, + Column, + ComboBox, + Form, + Grid, + InlineNotification, + Layer, + TextArea, + TextInput, +} from '@carbon/react'; +import { useTranslation } from 'react-i18next'; +import { Controller, type FieldErrors, useForm } from 'react-hook-form'; +import { zodResolver } from '@hookform/resolvers/zod'; +import { z } from 'zod'; +import styles from './medical-supply-order-form.scss'; +import { type Concept, ordersEqual, prepOrderPostData, useQuantityUnits } from '../resources'; +import { moduleName } from '../../constants'; +import { type MedicalSupplyOrderBasketItem } from '../types'; +import { NumberInput } from '@carbon/react'; +import { Select } from '@carbon/react'; +import { SelectSkeleton } from '@carbon/react'; + +export interface OrderFormProps extends DefaultPatientWorkspaceProps { + initialOrder: MedicalSupplyOrderBasketItem; + orderTypeUuid: string; + orderableConceptSets: Array; +} + +// Designs: +// https://app.zeplin.io/project/60d5947dd636aebbd63dce4c/screen/640b06c440ee3f7af8747620 +// https://app.zeplin.io/project/60d5947dd636aebbd63dce4c/screen/640b06d286e0aa7b0316db4a +export function OrderForm({ + initialOrder, + closeWorkspace, + closeWorkspaceWithSavedChanges, + promptBeforeClosing, + orderTypeUuid, +}: OrderFormProps) { + const { t } = useTranslation(); + const isTablet = useLayoutType() === 'tablet'; + const session = useSession(); + const isEditing = useMemo(() => initialOrder && initialOrder.action === 'REVISE', [initialOrder]); + const { orders, setOrders } = useOrderBasket(orderTypeUuid, prepOrderPostData); + const [showErrorNotification, setShowErrorNotification] = useState(false); + const { orderType } = useOrderType(orderTypeUuid); + const { concepts, isLoadingQuantityUnits, errorFetchingQuantityUnits } = useQuantityUnits(); + + const OrderFormSchema = useMemo( + () => + z.object({ + instructions: z.string().optional(), + urgency: z.string().refine((value) => value !== '', { + message: t('addLabOrderPriorityRequired', 'Priority is required'), + }), + quantity: z.number({ + required_error: t('quantityRequired', 'Quantity is required'), + invalid_type_error: t('quantityRequired', 'Quantity is required'), + }), + quantityUnits: z.object( + { + display: z.string(), + uuid: z.string(), + }, + { + required_error: t('quantityUnitsRequired', 'Quantity units is required'), + invalid_type_error: t('quantityUnitsRequired', 'Quantity units is required'), + }, + ), + accessionNumber: z.string().optional(), + concept: z.object( + { display: z.string(), uuid: z.string() }, + { + required_error: t('addOrderableConceptRequired', 'Orderable concept is required'), + invalid_type_error: t('addOrderableConceptRequired', 'Orderable concept is required'), + }, + ), + }), + [t], + ); + + const { + control, + handleSubmit, + formState: { errors, defaultValues, isDirty }, + } = useForm({ + mode: 'all', + resolver: zodResolver(OrderFormSchema), + defaultValues: { + ...initialOrder, + }, + }); + + const filterItemsByName = useCallback((menu) => { + return menu?.item?.value?.toLowerCase().includes(menu?.inputValue?.toLowerCase()); + }, []); + + const handleFormSubmission = useCallback( + (data: MedicalSupplyOrderBasketItem) => { + const finalizedOrder: MedicalSupplyOrderBasketItem = { + ...initialOrder, + ...data, + }; + finalizedOrder.orderer = session.currentProvider.uuid; + + const newOrders = [...orders]; + const existingOrder = orders.find((order) => ordersEqual(order, finalizedOrder)); + + if (existingOrder) { + newOrders[orders.indexOf(existingOrder)] = { + ...finalizedOrder, + // Incomplete orders should be marked completed on saving the form + isOrderIncomplete: false, + }; + } else { + newOrders.push(finalizedOrder); + } + + setOrders(newOrders); + + closeWorkspaceWithSavedChanges({ + onWorkspaceClose: () => launchPatientWorkspace('order-basket'), + }); + }, + [orders, setOrders, session?.currentProvider?.uuid, closeWorkspaceWithSavedChanges, initialOrder], + ); + + const cancelOrder = useCallback(() => { + setOrders(orders.filter((order) => order.concept.uuid !== defaultValues.concept.conceptUuid)); + closeWorkspace({ + onWorkspaceClose: () => launchPatientWorkspace('order-basket'), + }); + }, [closeWorkspace, orders, setOrders, defaultValues]); + + const onError = (errors: FieldErrors) => { + if (errors) { + setShowErrorNotification(true); + } + }; + + useEffect(() => { + promptBeforeClosing(() => isDirty); + }, [isDirty, promptBeforeClosing]); + + const responsiveSize = isTablet ? 'lg' : 'sm'; + + return ( + <> +
+
+ + + + + +

{initialOrder?.concept?.display}

+
+
+
+ + + + ( + + )} + /> + + + + + + + + ( + + field.onChange( + e.target.value != '' && e.target.value != null ? parseInt(e.target.value) : undefined, + ) + } + invalid={Boolean(error?.message)} + invalidText={error?.message} + label={t('quantity', 'Quantity')} + size={responsiveSize} + hideSteppers={!isTablet} + /> + )} + /> + + + + + + + + {isLoadingQuantityUnits ? ( + + ) : errorFetchingQuantityUnits ? ( + + ) : ( + ( + item?.display} + id="quantity" + onChange={({ selectedItem }) => field.onChange(selectedItem)} + invalid={Boolean(error?.message)} + invalidText={error?.message} + titleText={t('quantityUnit', 'Quantity unit')} + selectedItem={concepts.find(({ uuid }) => uuid === field.value?.uuid)} + size={responsiveSize} + /> + )} + /> + )} + + + + + + + + ( + onChange(selectedItem?.value || '')} + selectedItem={priorityOptions.find((option) => option.value === value) || null} + shouldFilterItem={filterItemsByName} + size={responsiveSize} + titleText={t('priority', 'Priority')} + /> + )} + /> + + + + + + + ( +