From 3b4a68df187c8173aad413580be1b81c23d82449 Mon Sep 17 00:00:00 2001 From: Laurent Ouma Date: Wed, 20 Nov 2024 21:28:32 +0300 Subject: [PATCH 1/2] Added preview model with printing capability --- package.json | 3 +- src/index.ts | 2 + .../prescription-tab-panel.component.tsx | 9 +- .../prescription-print-action.component.tsx | 34 ++++++ .../prescription-print-preview.modal.tsx | 83 ++++++++++++++ .../prescription-printout.component.tsx | 108 ++++++++++++++++++ .../print-prescription.scss | 12 ++ src/routes.json | 8 +- yarn.lock | 11 ++ 9 files changed, 267 insertions(+), 3 deletions(-) create mode 100644 src/print-prescription/prescription-print-action.component.tsx create mode 100644 src/print-prescription/prescription-print-preview.modal.tsx create mode 100644 src/print-prescription/prescription-printout.component.tsx create mode 100644 src/print-prescription/print-prescription.scss diff --git a/package.json b/package.json index 3e5b697..47e34fe 100644 --- a/package.json +++ b/package.json @@ -42,7 +42,8 @@ "dependencies": { "@carbon/react": "1.12.0", "classnames": "^2.5.1", - "lodash-es": "^4.17.21" + "lodash-es": "^4.17.21", + "react-to-print": "^2.14.13" }, "peerDependencies": { "@openmrs/esm-framework": "*", diff --git a/src/index.ts b/src/index.ts index 7d62586..3d6eada 100644 --- a/src/index.ts +++ b/src/index.ts @@ -4,6 +4,7 @@ import dispensingComponent from './dispensing.component'; import dispensingLinkComponent from './dispensing-link.component'; import dispensingDashboardComponent from './dashboard/dispensing-dashboard.component'; import dispensingLinkHomepageComponent from './dashboard/dispensing-dashboard-link.component'; +import PrescriptionPrintPreviewModal from './print-prescription/prescription-print-preview.modal'; export const importTranslation = require.context('../translations', false, /.json$/, 'lazy'); @@ -25,3 +26,4 @@ export function startupApp() { } export const dispensingDashboardLink = getSyncLifecycle(dispensingLinkHomepageComponent, options); +export const printPrescriptionPreviewModal = getSyncLifecycle(PrescriptionPrintPreviewModal, options); diff --git a/src/prescriptions/prescription-tab-panel.component.tsx b/src/prescriptions/prescription-tab-panel.component.tsx index 9c6bf65..a0bbd4a 100644 --- a/src/prescriptions/prescription-tab-panel.component.tsx +++ b/src/prescriptions/prescription-tab-panel.component.tsx @@ -23,6 +23,7 @@ import PrescriptionExpanded from './prescription-expanded.component'; import { usePrescriptionsTable } from '../medication-request/medication-request.resource'; import { type PharmacyConfig } from '../config-schema'; import styles from './prescriptions.scss'; +import PrescriptionPrintAction from '../print-prescription/prescription-print-action.component'; interface PrescriptionTabPanelProps { searchTerm: string; @@ -53,6 +54,7 @@ const PrescriptionTabPanel: React.FC = ({ searchTerm, { header: t('drugs', 'Drugs'), key: 'drugs' }, { header: t('lastDispenser', 'Last dispenser'), key: 'lastDispenser' }, { header: t('status', 'Status'), key: 'status' }, + { header: t('actions', 'Actions'), key: 'actions' }, ]; // add the locations column, if enabled @@ -66,6 +68,11 @@ const PrescriptionTabPanel: React.FC = ({ searchTerm, setNextOffSet(0); }, [searchTerm]); + const rows = prescriptionsTableRows?.map((row) => ({ + ...row, + actions: , + })); + return (
@@ -73,7 +80,7 @@ const PrescriptionTabPanel: React.FC = ({ searchTerm, {error &&

Error

} {prescriptionsTableRows && ( <> - + {({ rows, headers, getExpandHeaderProps, getHeaderProps, getRowProps, getTableProps }) => ( diff --git a/src/print-prescription/prescription-print-action.component.tsx b/src/print-prescription/prescription-print-action.component.tsx new file mode 100644 index 0000000..f48e08a --- /dev/null +++ b/src/print-prescription/prescription-print-action.component.tsx @@ -0,0 +1,34 @@ +import React from 'react'; +import { Button } from '@carbon/react'; +import { Printer } from '@carbon/react/icons'; +import { useTranslation } from 'react-i18next'; +import { showModal } from '@openmrs/esm-framework'; + +type PrescriptionPrintActionProps = { + encounterUuid: string; + patientUuid: string; + status: string; +}; + +const PrescriptionPrintAction: React.FC = ({ encounterUuid, patientUuid }) => { + const { t } = useTranslation(); + + return ( + + + + + + ); +}; + +export default PrescriptionPrintPreviewModal; diff --git a/src/print-prescription/prescription-printout.component.tsx b/src/print-prescription/prescription-printout.component.tsx new file mode 100644 index 0000000..32a6165 --- /dev/null +++ b/src/print-prescription/prescription-printout.component.tsx @@ -0,0 +1,108 @@ +import React from 'react'; +import { type DosageInstruction, type MedicationRequestBundle, type Quantity } from '../types'; +import { + getDosageInstruction, + getMedicationDisplay, + getMedicationReferenceOrCodeableConcept, + getQuantity, + getRefillsAllowed, +} from '../utils'; +import { + StructuredListWrapper, + StructuredListBody, + StructuredListRow, + StructuredListCell, +} from '@carbon/react'; +import { useTranslation } from 'react-i18next'; + +type PrescriptionsPrintoutProps = { + medicationrequests: Array; +}; + +const PrescriptionsPrintout: React.FC = ({ medicationrequests }) => { + const { t } = useTranslation(); + return ( + + {/* + + Patient Name Here + Facility name here + + */} + + {medicationrequests?.map((request, index) => { + const medicationEvent = request.request; + const dosageInstruction: DosageInstruction = getDosageInstruction(medicationEvent.dosageInstruction); + const quantity: Quantity = getQuantity(medicationEvent); + const refillsAllowed: number = getRefillsAllowed(medicationEvent); + return ( +
+ + + {getMedicationDisplay(getMedicationReferenceOrCodeableConcept(medicationEvent))} + + + + {dosageInstruction && ( + + + {t('dose', 'Dose').toUpperCase()} + {': '} + + {dosageInstruction?.doseAndRate?.map((doseAndRate, index) => { + return ( + + {doseAndRate?.doseQuantity?.value} {doseAndRate?.doseQuantity?.unit} + + ); + })} + {' '} + — {dosageInstruction?.route?.text} — {dosageInstruction?.timing?.code?.text}{' '} + {dosageInstruction?.timing?.repeat?.duration + ? 'for ' + + dosageInstruction?.timing?.repeat?.duration + + ' ' + + dosageInstruction?.timing?.repeat?.durationUnit + : ' '} + + + )} + {quantity && ( + + + {t('quantity', 'Quantity')} + {': '} + + {quantity.value} {quantity.unit} + + + + )} + + {(refillsAllowed || refillsAllowed === 0) && ( + + + {t('refills', 'Refills')} + {': '} {refillsAllowed} + + + )} + {dosageInstruction?.text && ( + + {dosageInstruction.text} + + )} + {dosageInstruction?.additionalInstruction?.length > 0 && ( + + {dosageInstruction?.additionalInstruction[0].text} + + )} +
+ ); + })} +
+
+ ); +}; + +export default PrescriptionsPrintout; diff --git a/src/print-prescription/print-prescription.scss b/src/print-prescription/print-prescription.scss new file mode 100644 index 0000000..e3eb303 --- /dev/null +++ b/src/print-prescription/print-prescription.scss @@ -0,0 +1,12 @@ +@use '@carbon/layout'; +@use '@carbon/type'; +@use '@carbon/colors'; + +.title { + @include type.type-style('heading-02'); +} + +.btnSet { + width: 100%; + background-color: red; +} diff --git a/src/routes.json b/src/routes.json index 81ac9a9..90c4417 100644 --- a/src/routes.json +++ b/src/routes.json @@ -35,5 +35,11 @@ "title": "Dispensing" } } + ], + "modals": [ + { + "name": "prescription-print-preview-modal", + "component": "printPrescriptionPreviewModal" + } ] -} +} \ No newline at end of file diff --git a/yarn.lock b/yarn.lock index eda98ac..327b680 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2945,6 +2945,7 @@ __metadata: react-dom: "npm:^18.2.0" react-i18next: "npm:^11.7.0" react-router-dom: "npm:^6.3.0" + react-to-print: "npm:^2.14.13" rxjs: "npm:^6.6.7" swr: "npm:^2.2.5" typescript: "npm:^4.3.2" @@ -14212,6 +14213,16 @@ __metadata: languageName: node linkType: hard +"react-to-print@npm:^2.14.13": + version: 2.15.1 + resolution: "react-to-print@npm:2.15.1" + peerDependencies: + react: ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 + react-dom: ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 + checksum: 10/0786fd5dd055b5b50f9a0dba99f0ccb30b9407b8e77748f5fc2557446ed0d0c65759e927cc2cdaa9e34f286838bbb230d3b0ba03d20b9754d0469fc4dac7c529 + languageName: node + linkType: hard + "react@npm:^18.1.0, react@npm:^18.2.0": version: 18.2.0 resolution: "react@npm:18.2.0" From 9ffad9db6699400e91fd3c05d22fb51586486b72 Mon Sep 17 00:00:00 2001 From: Laurent Ouma Date: Thu, 21 Nov 2024 14:56:11 +0300 Subject: [PATCH 2/2] Print preview and printing and printing done --- .../prescription-print-preview.modal.tsx | 16 +- .../prescription-printout.component.tsx | 178 ++++++++++-------- .../print-prescription.scss | 25 ++- 3 files changed, 129 insertions(+), 90 deletions(-) diff --git a/src/print-prescription/prescription-print-preview.modal.tsx b/src/print-prescription/prescription-print-preview.modal.tsx index d12dac6..99457f9 100644 --- a/src/print-prescription/prescription-print-preview.modal.tsx +++ b/src/print-prescription/prescription-print-preview.modal.tsx @@ -1,19 +1,19 @@ -import React, { useRef, useState } from 'react'; import { Button, + ButtonSet, InlineLoading, + InlineNotification, ModalBody, ModalFooter, ModalHeader, - ButtonSet, - InlineNotification, } from '@carbon/react'; -import styles from './print-prescription.scss'; -import { useTranslation } from 'react-i18next'; -import PrescriptionsPrintout from './prescription-printout.component'; -import { usePrescriptionDetails } from '../medication-request/medication-request.resource'; import { ErrorState } from '@openmrs/esm-framework'; +import React, { useRef, useState } from 'react'; +import { useTranslation } from 'react-i18next'; import { useReactToPrint } from 'react-to-print'; +import { usePrescriptionDetails } from '../medication-request/medication-request.resource'; +import PrescriptionsPrintout from './prescription-printout.component'; +import styles from './print-prescription.scss'; type PrescriptionPrintPreviewModalProps = { onClose: () => void; @@ -32,7 +32,6 @@ const PrescriptionPrintPreviewModal: React.FC(null); const [printError, setPrintError] = useState(null); - const handlePrint = useReactToPrint({ content: () => componentRef.current, onBeforeGetContent: () => { @@ -41,6 +40,7 @@ const PrescriptionPrintPreviewModal: React.FC { setPrintError(t('printError', 'An error occurred while printing. Please try again.')); }, + copyStyles: true, }); return ( diff --git a/src/print-prescription/prescription-printout.component.tsx b/src/print-prescription/prescription-printout.component.tsx index 32a6165..7201198 100644 --- a/src/print-prescription/prescription-printout.component.tsx +++ b/src/print-prescription/prescription-printout.component.tsx @@ -1,4 +1,7 @@ +import { Layer, StructuredListBody, StructuredListCell, StructuredListRow, StructuredListWrapper } from '@carbon/react'; +import { formatDate, parseDate, useSession } from '@openmrs/esm-framework'; import React from 'react'; +import { useTranslation } from 'react-i18next'; import { type DosageInstruction, type MedicationRequestBundle, type Quantity } from '../types'; import { getDosageInstruction, @@ -7,13 +10,7 @@ import { getQuantity, getRefillsAllowed, } from '../utils'; -import { - StructuredListWrapper, - StructuredListBody, - StructuredListRow, - StructuredListCell, -} from '@carbon/react'; -import { useTranslation } from 'react-i18next'; +import styles from './print-prescription.scss'; type PrescriptionsPrintoutProps = { medicationrequests: Array; @@ -21,87 +18,106 @@ type PrescriptionsPrintoutProps = { const PrescriptionsPrintout: React.FC = ({ medicationrequests }) => { const { t } = useTranslation(); + const { + sessionLocation: { display: facilityName }, + } = useSession(); + const patient = medicationrequests[0]?.request?.subject; return ( - - {/* + + + {/* Patient Name Here Facility name here */} - - {medicationrequests?.map((request, index) => { - const medicationEvent = request.request; - const dosageInstruction: DosageInstruction = getDosageInstruction(medicationEvent.dosageInstruction); - const quantity: Quantity = getQuantity(medicationEvent); - const refillsAllowed: number = getRefillsAllowed(medicationEvent); - return ( -
- - - {getMedicationDisplay(getMedicationReferenceOrCodeableConcept(medicationEvent))} - - - - {dosageInstruction && ( - - - {t('dose', 'Dose').toUpperCase()} - {': '} - - {dosageInstruction?.doseAndRate?.map((doseAndRate, index) => { - return ( - - {doseAndRate?.doseQuantity?.value} {doseAndRate?.doseQuantity?.unit} - - ); - })} - {' '} - — {dosageInstruction?.route?.text} — {dosageInstruction?.timing?.code?.text}{' '} - {dosageInstruction?.timing?.repeat?.duration - ? 'for ' + - dosageInstruction?.timing?.repeat?.duration + - ' ' + - dosageInstruction?.timing?.repeat?.durationUnit - : ' '} - - - )} - {quantity && ( - - - {t('quantity', 'Quantity')} - {': '} - - {quantity.value} {quantity.unit} - - - - )} - - {(refillsAllowed || refillsAllowed === 0) && ( - - - {t('refills', 'Refills')} - {': '} {refillsAllowed} - - - )} - {dosageInstruction?.text && ( - - {dosageInstruction.text} - - )} - {dosageInstruction?.additionalInstruction?.length > 0 && ( - - {dosageInstruction?.additionalInstruction[0].text} - + + + +
+
+

Prescription Instructions

+ {patient && ( +

+ {patient.display + .match(/^([A-Za-z\s]+)\s\(/) + ?.at(1) + ?.toUpperCase()} +

)} -
- ); - })} -
-
+
+
+ + + {medicationrequests?.map((request, index) => { + const medicationEvent = request.request; + const dosageInstruction: DosageInstruction = getDosageInstruction(medicationEvent.dosageInstruction); + const quantity: Quantity = getQuantity(medicationEvent); + const refillsAllowed: number = getRefillsAllowed(medicationEvent); + return ( +
+ {dosageInstruction && ( + + +

+ + {getMedicationDisplay(getMedicationReferenceOrCodeableConcept(medicationEvent))} + +

+
+

+ {t('dose', 'Dose')} + {': '} + + {dosageInstruction?.doseAndRate?.map((doseAndRate, index) => { + return ( + + {doseAndRate?.doseQuantity?.value} {doseAndRate?.doseQuantity?.unit} + + ); + })} + {' '} + — {dosageInstruction?.route?.text} — {dosageInstruction?.timing?.code?.text}{' '} + {dosageInstruction?.timing?.repeat?.duration + ? 'for ' + + dosageInstruction?.timing?.repeat?.duration + + ' ' + + dosageInstruction?.timing?.repeat?.durationUnit + : ' '} + {quantity && ( +

+ {t('quantity', 'Quantity')} + {': '} + + {quantity.value} {quantity.unit} + +

+ )} +

+

+ {t('datePrescribed', 'Date prescribed')} + {': '} {formatDate(parseDate((request.request as any).authoredOn))} +

+ {(refillsAllowed || refillsAllowed === 0) && ( +

+ {t('refills', 'Refills')} + {': '} {refillsAllowed} +

+ )} + {dosageInstruction?.text &&

{dosageInstruction.text}

} + {dosageInstruction?.additionalInstruction?.length > 0 && ( +

{dosageInstruction?.additionalInstruction[0].text}

+ )} +
+
+ )} +
+ ); + })} +

{facilityName}

+ +
+ ); }; diff --git a/src/print-prescription/print-prescription.scss b/src/print-prescription/print-prescription.scss index e3eb303..5947119 100644 --- a/src/print-prescription/print-prescription.scss +++ b/src/print-prescription/print-prescription.scss @@ -8,5 +8,28 @@ .btnSet { width: 100%; - background-color: red; +} + +.faintText { + color: colors.$gray-50; + @include type.type-style('heading-01'); +} + +.printOutContainer { + background-color: colors.$gray-20; + padding: layout.$spacing-05; + width: 320px; +} + +.printoutTitle { + @include type.type-style('heading-03'); + text-align: center; + font-weight: bold; +} + +.facilityName { + color: colors.$gray-50; + text-align: end; + padding: layout.$spacing-05 0 layout.$spacing-05 0; + @include type.type-style('label-01'); }