From 462a6cdbc45c0c8ab2b23beff92979cdbfcc18a8 Mon Sep 17 00:00:00 2001 From: jnsereko Date: Tue, 23 Jul 2024 00:49:53 +0300 Subject: [PATCH] (feat) Add configurable ability to print multiple stickers on the same page --- package.json | 2 +- ...t-identifier-sticker-content.component.tsx | 68 ++++++ .../print-identifier-sticker-content.scss | 41 ++++ .../print-identifier-sticker-modal.test.tsx | 70 +++--- .../print-identifier-sticker.component.tsx | 75 +++++++ .../print-identifier-sticker.modal.tsx | 203 +++++++----------- .../banner-tags/print-identifier-sticker.scss | 26 +++ .../src/config-schema.ts | 39 +++- .../translations/en.json | 5 + .../print-modal/print-modal.extension.tsx | 6 +- .../medications-details-table.component.tsx | 22 +- .../orders-details-table.component.tsx | 24 +-- .../src/vitals/vitals-overview.component.tsx | 22 +- yarn.lock | 13 +- 14 files changed, 380 insertions(+), 236 deletions(-) create mode 100644 packages/esm-patient-banner-app/src/banner-tags/print-identifier-sticker-content.component.tsx create mode 100644 packages/esm-patient-banner-app/src/banner-tags/print-identifier-sticker-content.scss create mode 100644 packages/esm-patient-banner-app/src/banner-tags/print-identifier-sticker.component.tsx diff --git a/package.json b/package.json index 8754f71b2b..db838568cb 100644 --- a/package.json +++ b/package.json @@ -19,7 +19,7 @@ "@hookform/resolvers": "^3.3.1", "classnames": "^2.3.2", "react-hook-form": "^7.46.2", - "react-to-print": "^2.14.13", + "react-to-print": "^3.0.0-beta-1", "zod": "^3.22.2" }, "devDependencies": { diff --git a/packages/esm-patient-banner-app/src/banner-tags/print-identifier-sticker-content.component.tsx b/packages/esm-patient-banner-app/src/banner-tags/print-identifier-sticker-content.component.tsx new file mode 100644 index 0000000000..710ca77098 --- /dev/null +++ b/packages/esm-patient-banner-app/src/banner-tags/print-identifier-sticker-content.component.tsx @@ -0,0 +1,68 @@ +import React, { forwardRef, useEffect, useImperativeHandle, useRef } from 'react'; +import { useConfig } from '@openmrs/esm-framework'; +import { type ConfigObject } from '../config-schema'; +import IdentifierSticker from './print-identifier-sticker.component'; +import styles from './print-identifier-sticker-content.scss'; + +interface PrintIdentifierStickerContentProps { + labels: Array<{}>; + numberOfLabelColumns: number; + numberOfLabelRowsPerPage: number; + patient: fhir.Patient; +} + +const PrintIdentifierStickerContent = forwardRef( + ({ labels, numberOfLabelColumns, numberOfLabelRowsPerPage, patient }, ref) => { + const { printIdentifierStickerWidth, printIdentifierStickerHeight, printIdentifierStickerPaperSize } = + useConfig(); + const divRef = useRef(); + + useImperativeHandle(ref, () => divRef.current, []); + + useEffect(() => { + if (divRef.current) { + const style = divRef.current.style; + style.setProperty('--omrs-print-label-paper-size', printIdentifierStickerPaperSize); + style.setProperty('--omrs-print-label-colums', numberOfLabelColumns.toString()); + style.setProperty('--omrs-print-label-rows', numberOfLabelRowsPerPage.toString()); + style.setProperty('--omrs-print-label-sticker-height', printIdentifierStickerHeight); + style.setProperty('--omrs-print-label-sticker-width', printIdentifierStickerWidth); + } + }, [ + numberOfLabelColumns, + numberOfLabelRowsPerPage, + printIdentifierStickerHeight, + printIdentifierStickerPaperSize, + printIdentifierStickerWidth, + ]); + + const maxLabelsPerPage = numberOfLabelRowsPerPage * numberOfLabelColumns; + const pages: Array = []; + + for (let i = 0; i < labels.length; i += maxLabelsPerPage) { + pages.push(labels.slice(i, i + maxLabelsPerPage)); + } + + if (numberOfLabelColumns < 1 || numberOfLabelRowsPerPage < 1 || labels.length < 1) { + return; + } + + return ( +
+ {pages.map((pageLabels, pageIndex) => ( +
+
+ {pageLabels.map((label, index) => ( +
+ +
+ ))} +
+
+ ))} +
+ ); + }, +); + +export default PrintIdentifierStickerContent; diff --git a/packages/esm-patient-banner-app/src/banner-tags/print-identifier-sticker-content.scss b/packages/esm-patient-banner-app/src/banner-tags/print-identifier-sticker-content.scss new file mode 100644 index 0000000000..427dfc991b --- /dev/null +++ b/packages/esm-patient-banner-app/src/banner-tags/print-identifier-sticker-content.scss @@ -0,0 +1,41 @@ +@use '@carbon/layout'; +@use '@carbon/type'; +@use '@openmrs/esm-styleguide/src/vars' as *; + +.printRoot { + @media print { + @page { + size: var(--omrs-print-label-paper-size, auto); + } + + html, + body { + height: initial !important; + overflow: initial !important; + background-color: white; + } + } + + .labelsContainer { + grid-template-columns: repeat(var(--omrs-print-label-colums, 1), 1fr); + grid-template-rows: repeat(var(--omrs-print-label-rows, 1), auto); + } +} + +.printContainer { + height: var(--omrs-print-label-sticker-height, 11rem); + width: var(--omrs-print-label-sticker-width, 13rem); + background-color: $ui-01; +} + +.pageBreak { + page-break-after: always; +} + +.labelsContainer { + display: grid; + column-gap: 1.3rem; + row-gap: 1rem; + place-items: center; + background-color: white; +} diff --git a/packages/esm-patient-banner-app/src/banner-tags/print-identifier-sticker-modal.test.tsx b/packages/esm-patient-banner-app/src/banner-tags/print-identifier-sticker-modal.test.tsx index 9092801ae8..42a742c18d 100644 --- a/packages/esm-patient-banner-app/src/banner-tags/print-identifier-sticker-modal.test.tsx +++ b/packages/esm-patient-banner-app/src/banner-tags/print-identifier-sticker-modal.test.tsx @@ -1,64 +1,46 @@ import React from 'react'; import userEvent from '@testing-library/user-event'; import { render, screen } from '@testing-library/react'; -import { useReactToPrint } from 'react-to-print'; import { getDefaultsFromConfigSchema, useConfig } from '@openmrs/esm-framework'; import { configSchema, type ConfigObject } from '../config-schema'; import { mockPatient } from 'tools'; -import PrintIdentifierSticker from './print-identifier-sticker.modal'; +import PrintIdentifierStickerModal from './print-identifier-sticker.modal'; const mockCloseModal = jest.fn(); -const mockUseReactToPrint = jest.mocked(useReactToPrint); const mockUseConfig = jest.mocked(useConfig); -jest.mock('react-to-print', () => { - const originalModule = jest.requireActual('react-to-print'); - - return { - ...originalModule, - useReactToPrint: jest.fn(), - }; -}); - mockUseConfig.mockReturnValue({ ...getDefaultsFromConfigSchema(configSchema), printIdentifierStickerFields: ['name', 'identifier', 'age', 'dateOfBirth', 'gender'], }); describe('PrintIdentifierSticker', () => { - test('renders the component', () => { - render(); - - expect(screen.getByText(/Print Identifier Sticker/i)).toBeInTheDocument(); - expect(screen.getByText('John Wilson')).toBeInTheDocument(); - expect(screen.getByText('100GEJ')).toBeInTheDocument(); - expect(screen.getByText('1972-04-04')).toBeInTheDocument(); - }); - - test('calls closeModal when cancel button is clicked', async () => { + test('renders a modal with patient details and print options', async () => { const user = userEvent.setup(); - render(); - - const cancelButton = screen.getByRole('button', { name: /Cancel/i }); - expect(cancelButton).toBeInTheDocument(); - - await user.click(cancelButton); - expect(mockCloseModal).toHaveBeenCalled(); - }); - - test('calls the print function when print button is clicked', async () => { - const handlePrint = jest.fn(); - mockUseReactToPrint.mockReturnValue(handlePrint); - - const user = userEvent.setup(); - - render(); - - const printButton = screen.getByRole('button', { name: /Print/i }); - expect(printButton).toBeInTheDocument(); - - await user.click(printButton); - expect(handlePrint).toHaveBeenCalled(); + render(); + + expect(screen.getByRole('heading', { name: /print identifier sticker/i })).toBeInTheDocument(); + expect(screen.getByRole('button', { name: /show preview/i })).toBeInTheDocument(); + expect(screen.getByRole('button', { name: /cancel/i })).toBeInTheDocument(); + expect(screen.getByRole('button', { name: /print/i })).toBeInTheDocument(); + expect(screen.getByRole('spinbutton', { name: /no. of patient id sticker columns/i })).toHaveValue(3); + expect(screen.getByRole('spinbutton', { name: /no. of patient id sticker rows per page/i })).toHaveValue(5); + expect(screen.getByRole('spinbutton', { name: /no. of patient id stickers/i })).toHaveValue(30); + + const modalBody = screen.getByRole('region'); + expect(modalBody).toHaveTextContent(/john wilson/i); + expect(modalBody).toHaveTextContent(/old identification number/i); + expect(modalBody).toHaveTextContent(/100732he/i); + expect(modalBody).toHaveTextContent(/openmrs id/i); + expect(modalBody).toHaveTextContent(/100gej/i); + expect(modalBody).toHaveTextContent(/sex/i); + expect(modalBody).toHaveTextContent(/male/i); + expect(modalBody).toHaveTextContent(/dob/i); + expect(modalBody).toHaveTextContent(/1972-04-04/i); + expect(modalBody).toHaveTextContent(/age/i); + expect(modalBody).toHaveTextContent(/52 yrs/i); + await user.click(screen.getByRole('button', { name: /show preview/i })); + expect(screen.getByRole('button', { name: /hide preview/i })).toBeInTheDocument(); }); }); diff --git a/packages/esm-patient-banner-app/src/banner-tags/print-identifier-sticker.component.tsx b/packages/esm-patient-banner-app/src/banner-tags/print-identifier-sticker.component.tsx new file mode 100644 index 0000000000..ae24657d18 --- /dev/null +++ b/packages/esm-patient-banner-app/src/banner-tags/print-identifier-sticker.component.tsx @@ -0,0 +1,75 @@ +import React, { forwardRef, useMemo } from 'react'; +import { useTranslation } from 'react-i18next'; +import { age, getPatientName, useConfig, getCoreTranslation } from '@openmrs/esm-framework'; +import { type ConfigObject } from '../config-schema'; +import styles from './print-identifier-sticker.scss'; + +interface IdentifierStickerProps { + patient: fhir.Patient; +} + +const getGender = (gender: string): string => { + switch (gender) { + case 'male': + return getCoreTranslation('male', 'Male'); + case 'female': + return getCoreTranslation('female', 'Female'); + case 'other': + return getCoreTranslation('other', 'Other'); + case 'unknown': + return getCoreTranslation('unknown', 'Unknown'); + default: + return gender; + } +}; + +const IdentifierSticker = forwardRef(({ patient }, ref) => { + const { t } = useTranslation(); + const { printIdentifierStickerFields, excludePatientIdentifierCodeTypes } = useConfig(); + + const patientDetails = useMemo(() => { + if (!patient) { + return {}; + } + + const identifiers = + patient.identifier?.filter( + (identifier) => !excludePatientIdentifierCodeTypes?.uuids.includes(identifier.type.coding[0].code), + ) ?? []; + + return { + address: patient.address, + age: age(patient.birthDate), + dateOfBirth: patient.birthDate, + gender: getGender(patient.gender), + id: patient.id, + identifiers: [...identifiers], + name: patient ? getPatientName(patient) : '', + photo: patient.photo, + }; + }, [excludePatientIdentifierCodeTypes?.uuids, patient]); + + return ( +
+ {printIdentifierStickerFields.includes('name') &&
{patientDetails.name}
} + {patientDetails.identifiers.map((identifier) => { + return ( +

+ {identifier?.type?.text}: {identifier?.value} +

+ ); + })} +

+ {getCoreTranslation('sex', 'Sex')}: {patientDetails.gender} +

+

+ {t('dob', 'DOB')}: {patientDetails.dateOfBirth} +

+

+ {getCoreTranslation('age', 'Age')}: {patientDetails.age} +

+
+ ); +}); + +export default IdentifierSticker; diff --git a/packages/esm-patient-banner-app/src/banner-tags/print-identifier-sticker.modal.tsx b/packages/esm-patient-banner-app/src/banner-tags/print-identifier-sticker.modal.tsx index 47dac29857..9402352c3b 100644 --- a/packages/esm-patient-banner-app/src/banner-tags/print-identifier-sticker.modal.tsx +++ b/packages/esm-patient-banner-app/src/banner-tags/print-identifier-sticker.modal.tsx @@ -1,108 +1,47 @@ -import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; -import { useTranslation, type TFunction } from 'react-i18next'; -import { useReactToPrint } from 'react-to-print'; -import { Button, InlineLoading, ModalBody, ModalFooter, ModalHeader } from '@carbon/react'; -import { age, getPatientName, showSnackbar, useConfig, getCoreTranslation } from '@openmrs/esm-framework'; +import React, { useCallback, useRef, useState } from 'react'; +import classNames from 'classnames'; +import { useTranslation } from 'react-i18next'; +import { useReactToPrint, type UseReactToPrintOptions } from 'react-to-print'; +import { Button, InlineLoading, ModalBody, ModalFooter, ModalHeader, NumberInput, Stack } from '@carbon/react'; +import { getPatientName, showSnackbar, useConfig, getCoreTranslation } from '@openmrs/esm-framework'; import { type ConfigObject } from '../config-schema'; +import IdentifierSticker from './print-identifier-sticker.component'; +import PrintIdentifierStickerContent from './print-identifier-sticker-content.component'; import styles from './print-identifier-sticker.scss'; -interface PrintIdentifierStickerProps { +interface PrintIdentifierStickerModalProps { closeModal: () => void; patient: fhir.Patient; } -interface PrintComponentProps extends Partial { - patientDetails: { - address?: fhir.Address[]; - age?: string; - dateOfBirth?: string; - gender?: string; - id?: string; - identifiers?: fhir.Identifier[]; - name?: string; - photo?: fhir.Attachment[]; - }; - t: TFunction; -} - -const PrintIdentifierSticker: React.FC = ({ closeModal, patient }) => { +const PrintIdentifierStickerModal: React.FC = ({ closeModal, patient }) => { const { t } = useTranslation(); - const { printIdentifierStickerFields, printIdentifierStickerSize, excludePatientIdentifierCodeTypes } = + const { numberOfPatientIdStickers, numberOfPatientIdStickerRowsPerPage, numberOfPatientIdStickerColumns } = useConfig(); - const contentToPrintRef = useRef(null); - const onBeforeGetContentResolve = useRef<() => void | null>(null); + const contentToPrintRef = useRef(null); + const [numberOfLabelColumns, setNumberOfLabelColumns] = useState(numberOfPatientIdStickerColumns); + const [numberOfLabelRowsPerPage, setNumberOfLabelRowsPerPage] = useState(numberOfPatientIdStickerRowsPerPage); + const [numberOfLabels, setNumberOfLabels] = useState(numberOfPatientIdStickers); const [isPrinting, setIsPrinting] = useState(false); const headerTitle = t('patientIdentifierSticker', 'Patient identifier sticker'); + const [isPreviewVisible, setIsPreviewVisible] = useState(false); - useEffect(() => { - if (isPrinting && onBeforeGetContentResolve.current) { - onBeforeGetContentResolve.current(); - } - }, [isPrinting]); - - const patientDetails = useMemo(() => { - if (!patient) { - return {}; - } - - const getGender = (gender: string): string => { - switch (gender) { - case 'male': - return getCoreTranslation('male', 'Male'); - case 'female': - return getCoreTranslation('female', 'Female'); - case 'other': - return getCoreTranslation('other', 'Other'); - case 'unknown': - return getCoreTranslation('unknown', 'Unknown'); - default: - return gender; - } - }; + const labels = Array.from({ length: numberOfLabels }); - const identifiers = - patient.identifier?.filter( - (identifier) => !excludePatientIdentifierCodeTypes?.uuids.includes(identifier.type.coding[0].code), - ) ?? []; - - return { - address: patient.address, - age: age(patient.birthDate), - dateOfBirth: patient.birthDate, - gender: getGender(patient.gender), - id: patient.id, - identifiers: [...identifiers], - name: patient ? getPatientName(patient) : '', - photo: patient.photo, - }; - }, [excludePatientIdentifierCodeTypes?.uuids, patient]); - - const handleBeforeGetContent = useCallback( - () => - new Promise((resolve) => { - if (patient && headerTitle) { - onBeforeGetContentResolve.current = resolve; - setIsPrinting(true); - } - }), - [headerTitle, patient], - ); + const handleBeforePrint = useCallback(() => setIsPrinting(true), []); const handleAfterPrint = useCallback(() => { - onBeforeGetContentResolve.current = null; setIsPrinting(false); closeModal(); }, [closeModal]); - const handlePrintError = useCallback((errorLocation, error) => { - onBeforeGetContentResolve.current = null; - + const handlePrintError = useCallback((errorLocation, error) => { showSnackbar({ isLowContrast: false, kind: 'error', title: getCoreTranslation('printError', 'Print error'), subtitle: - getCoreTranslation('printErrorExplainer', 'An error occurred in "{{errorLocation}}": ', { errorLocation }) + + getCoreTranslation('printErrorExplainer', 'An error occurred during "{{errorLocation}}": ', { errorLocation }) + error, }); @@ -110,10 +49,9 @@ const PrintIdentifierSticker: React.FC = ({ closeMo }, []); const handlePrint = useReactToPrint({ - content: () => contentToPrintRef.current, - documentTitle: `${patientDetails.name} - ${headerTitle}`, + contentRef: contentToPrintRef, + documentTitle: `${patient ? getPatientName(patient) : ''} - ${headerTitle}`, onAfterPrint: handleAfterPrint, - onBeforeGetContent: handleBeforeGetContent, onPrintError: handlePrintError, }); @@ -123,21 +61,62 @@ const PrintIdentifierSticker: React.FC = ({ closeMo closeModal={closeModal} title={getCoreTranslation('printIdentifierSticker', 'Print identifier sticker')} /> - -
- - + + ) => + setNumberOfLabelColumns(parseInt(event.target.value || '1')) + } + value={numberOfLabelColumns} + /> + ) => + setNumberOfLabelRowsPerPage(parseInt(event.target.value || '1')) + } + value={numberOfLabelRowsPerPage} /> -
+ ) => + setNumberOfLabels(parseInt(event.target.value || '1')) + } + value={numberOfLabels} + /> +
+ + + + +
+
+
+ +
+
+