Skip to content

Commit

Permalink
O3-3276: Add support for printing patient identification stickers
Browse files Browse the repository at this point in the history
Update packages/esm-patient-banner-app/src/config-schema.ts

Co-authored-by: Dennis Kigen <[email protected]>

Update packages/esm-patient-banner-app/src/config-schema.ts

Co-authored-by: Dennis Kigen <[email protected]>

Update packages/esm-patient-banner-app/src/config-schema.ts

Co-authored-by: Dennis Kigen <[email protected]>

Update packages/esm-patient-banner-app/src/banner/patient-banner.component.tsx

Co-authored-by: Dennis Kigen <[email protected]>

Update packages/esm-patient-banner-app/src/banner-tags/print-identifier-sticker.test.tsx

Co-authored-by: Dennis Kigen <[email protected]>

Update packages/esm-patient-banner-app/src/banner-tags/print-identifier-sticker.component.tsx

Co-authored-by: Dennis Kigen <[email protected]>

Update packages/esm-patient-banner-app/src/banner-tags/print-identifier-sticker.component.tsx

Co-authored-by: Dennis Kigen <[email protected]>

Update packages/esm-patient-banner-app/src/banner-tags/print-identifier-sticker.test.tsx

Co-authored-by: Dennis Kigen <[email protected]>

O3-3276: Add support for printing patient identification stickers

Update packages/esm-patient-banner-app/src/banner-tags/print-identifier-sticker.test.tsx

Co-authored-by: Dennis Kigen <[email protected]>

Update packages/esm-patient-banner-app/src/banner-tags/print-identifier-sticker.test.tsx

Co-authored-by: Dennis Kigen <[email protected]>

Update packages/esm-patient-banner-app/src/banner-tags/print-identifier-sticker.test.tsx

Co-authored-by: Dennis Kigen <[email protected]>

Update packages/esm-patient-banner-app/src/banner-tags/print-identifier-sticker.test.tsx

Co-authored-by: Dennis Kigen <[email protected]>

Update packages/esm-patient-banner-app/src/banner-tags/print-identifier-sticker.scss

Co-authored-by: Dennis Kigen <[email protected]>

Update packages/esm-patient-banner-app/translations/en.json

Co-authored-by: Dennis Kigen <[email protected]>

Update packages/esm-patient-banner-app/src/config-schema.ts

Co-authored-by: Dennis Kigen <[email protected]>

Update packages/esm-patient-banner-app/src/banner-tags/print-identifier-sticker.component.tsx

Co-authored-by: Dennis Kigen <[email protected]>

Update packages/esm-patient-banner-app/src/banner-tags/print-identifier-sticker.scss

Co-authored-by: Dennis Kigen <[email protected]>

Update packages/esm-patient-banner-app/src/banner-tags/print-identifier-sticker.scss

Co-authored-by: Dennis Kigen <[email protected]>

Update packages/esm-patient-banner-app/src/banner-tags/print-identifier-sticker.test.tsx

Co-authored-by: Dennis Kigen <[email protected]>

Update packages/esm-patient-banner-app/src/banner-tags/print-identifier-sticker.test.tsx

Co-authored-by: Dennis Kigen <[email protected]>

Update packages/esm-patient-banner-app/src/banner-tags/print-identifier-sticker.scss

Co-authored-by: Dennis Kigen <[email protected]>

Update packages/esm-patient-banner-app/src/banner-tags/print-identifier-sticker.test.tsx

Co-authored-by: Dennis Kigen <[email protected]>

Update packages/esm-patient-banner-app/translations/en.json

Co-authored-by: Dennis Kigen <[email protected]>

Update packages/esm-patient-banner-app/src/banner-tags/print-identifier-sticker.test.tsx

Co-authored-by: Dennis Kigen <[email protected]>

Update packages/esm-patient-banner-app/src/banner-tags/print-identifier-sticker.component.tsx

Co-authored-by: Dennis Kigen <[email protected]>

add retry attemp on printing

Update packages/esm-patient-banner-app/src/banner-tags/print-identifier-sticker.component.tsx

Co-authored-by: Ian <[email protected]>
  • Loading branch information
2 people authored and denniskigen committed Jun 13, 2024
1 parent cb9ad24 commit a529522
Show file tree
Hide file tree
Showing 9 changed files with 385 additions and 12 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,198 @@
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { Button, ModalBody, ModalFooter, ModalHeader } from '@carbon/react';
import styles from './print-identifier-sticker.scss';
import { useReactToPrint } from 'react-to-print';
import { Column } from '@carbon/react';
import { Grid } from '@carbon/react';
import { age, displayName, showSnackbar, useConfig } from '@openmrs/esm-framework';

interface PrintIdentifierStickerProps {
patient: fhir.Patient;
closeModal: () => void;
}

const PrintIdentifierSticker: React.FC<PrintIdentifierStickerProps> = ({ patient, closeModal }) => {
const { t } = useTranslation();
const [isPrinting, setIsPrinting] = useState(false);
const contentToPrintRef = useRef(null);
const [printRetryCount, setPrintRetryCount] = useState(0);
const { printIdentifierStickerFields, printIdentifierStickerSize, excludePatientIdentifierCodeTypes } = useConfig();

const headerTitle = t('patientIdentifierSticker', 'Patient Identifier Sticker');

const onBeforeGetContentResolve = useRef(null);

useEffect(() => {
if (isPrinting && onBeforeGetContentResolve.current) {
onBeforeGetContentResolve.current();
}
}, [isPrinting]);

const patientDetails = useMemo(() => {
const getGender = (gender: string): string => {
switch (gender) {
case 'male':
return t('male', 'Male');
case 'female':
return t('female', 'Female');
case 'other':
return t('other', 'Other');
case 'unknown':
return t('unknown', 'Unknown');
default:
return gender;
}
};

const identifiers =
patient?.identifier?.filter(
(identifier) => !excludePatientIdentifierCodeTypes?.uuids.includes(identifier.type.coding[0].code),
) ?? [];

return {
id: patient?.id,
photo: patient?.photo,
name: patient ? displayName(patient) : '',
dateOfBirth: patient.birthDate,
age: age(patient?.birthDate),
gender: getGender(patient?.gender),
address: patient?.address,
identifiers: identifiers?.length ? identifiers.map(({ value, type }) => ({ value, type })) : [],
};
}, [patient, t, excludePatientIdentifierCodeTypes?.uuids]);
const handleBeforeGetContent = useCallback(() => {
return new Promise<void>((resolve) => {
if (patient && headerTitle) {
onBeforeGetContentResolve.current = resolve;
setIsPrinting(true);
const printStyles = `@media print { @page { size: ${printIdentifierStickerSize}; } }`;

const style = document.createElement('style');
style.appendChild(document.createTextNode(printStyles));

document.head.appendChild(style);
}
});
}, [patient, printIdentifierStickerSize, headerTitle]);

const handleAfterPrint = useCallback(() => {
onBeforeGetContentResolve.current = null;
setIsPrinting(false);
closeModal();
}, [closeModal]);

const handlePrint = useReactToPrint({
content: () => contentToPrintRef.current,
documentTitle: `${patientDetails.name} - ${headerTitle}`,
onBeforeGetContent: handleBeforeGetContent,
onAfterPrint: handleAfterPrint,
onPrintError: (errorLocation, error) => {
new Promise<void>((resolve) => {
if (error) {
if (printRetryCount < 2) {
setPrintRetryCount(printRetryCount + 1);
handleBeforeGetContent().then(() => {
handlePrint();
});
} else {
showSnackbar({
title: t('printFailed', 'Print Failed'),
subtitle: `${t('printingFailed', 'Printing has failed after 3 attempts')}`,
kind: 'error',
isLowContrast: true,
});
setPrintRetryCount(1);
}
resolve();
}
});
},
});

const renderElementsInPairs = (elements) => {
const pairs = [];
let currentPair = [];

const getKey = (element) => {
if (element?.props?.children?.key?.startsWith('identifier-text')) {
return 'identifier';
}
return element?.key;
};

const filteredElements = elements.filter((element) => {
return printIdentifierStickerFields.includes(getKey(element));
});

filteredElements.forEach((element, index) => {
if (element) {
currentPair.push(element);
if (currentPair.length === 2) {
pairs.push(currentPair);
currentPair = [];
}
}
});

if (currentPair.length === 1) {
pairs.push(currentPair);
}

return pairs;
};

return (
<div>
<ModalHeader closeModal={closeModal} title={t('printIdentifierSticker', 'Print identifier sticker')} />
<ModalBody>
<div className={styles.stickerContainer} ref={contentToPrintRef}>
{printIdentifierStickerFields.includes('name') && (
<div key="name" className={styles.patientName}>
{patientDetails?.name}
</div>
)}
{renderElementsInPairs(
[
patientDetails?.identifiers?.map((identifier, index) => (
<div key={`identifier-${index}`}>
<p key={`identifier-text-${index}`}>
{identifier?.type?.text}: <strong>{identifier?.value}</strong>
</p>
</div>
)),
<p key="gender">
{t('sex', 'Sex')}: <strong>{patientDetails?.gender}</strong>
</p>,
<p key="dateOfBirth">
{t('bod', 'DOB')}: <strong>{patientDetails?.dateOfBirth}</strong>
</p>,
<p key="age">
{t('age', 'Age')}: <strong>{patientDetails?.age}</strong>
</p>,
].flat(),
).map((pair, index) => (
<Grid className={styles.gridRow} key={`grid-${index}`}>
<Column lg={8} md={4} sm={4}>
<div key={`pair-0-${index}`}>{pair[0]}</div>
</Column>
<Column lg={8} md={4} sm={4}>
<div key={`pair-1-${index}`}>{pair[1] || <div />}</div>
</Column>
</Grid>
))}
</div>
</ModalBody>
<ModalFooter>
<Button kind="secondary" onClick={closeModal}>
{t('cancel', 'Cancel')}
</Button>
<Button disabled={isPrinting} onClick={handlePrint} kind="primary">
{t('print', 'Print')}
</Button>
</ModalFooter>
</div>
);
};

export default PrintIdentifierSticker;
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
@use '@carbon/layout';
@use '@carbon/type';

.stickerContainer {
padding: 1rem 1rem 0 1rem;
}

.row {
margin: layout.$spacing-03 0rem 0rem;
display: flex;
flex-flow: row wrap;
gap: layout.$spacing-05;
}

.gridRow {
padding: 0px;
display: flex;
flex-direction: row;
justify-content: space-between;

div {
margin-left: 0px;
margin-bottom: 5px;
}
}

.patientName {
font-size: 1.5rem;
font-weight: bolder;
margin-bottom: 5px;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import React from 'react';
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { mockPatient } from 'tools';
import { useReactToPrint } from 'react-to-print';
import PrintIdentifierSticker from './print-identifier-sticker.component';

const mockedCloseModal = jest.fn();
const mockedUseReactToPrint = jest.mocked(useReactToPrint);

jest.mock('react-to-print', () => {
const originalModule = jest.requireActual('react-to-print');

return {
...originalModule,
useReactToPrint: jest.fn(),
};
});

jest.mock('@openmrs/esm-framework', () => {
const originalModule = jest.requireActual('@openmrs/esm-framework');

return {
...originalModule,
useConfig: jest.fn().mockImplementation(() => ({
printIdentifierStickerFields: ['name', 'identifier', 'age', 'dateOfBirth', 'gender'],
})),
};
});

describe('PrintIdentifierSticker', () => {
test('renders the component', () => {
renderPrintIdentifierSticker();

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 () => {
const user = userEvent.setup();

renderPrintIdentifierSticker();

const cancelButton = screen.getByRole('button', { name: /Cancel/i });
expect(cancelButton).toBeInTheDocument();

await user.click(cancelButton);
expect(mockedCloseModal).toHaveBeenCalled();
});

test('calls the print function when print button is clicked', async () => {
const handlePrint = jest.fn();
mockedUseReactToPrint.mockReturnValue(handlePrint);

const user = userEvent.setup();

renderPrintIdentifierSticker();

const printButton = screen.getByRole('button', { name: /Print/i });
expect(printButton).toBeInTheDocument();

await user.click(printButton);
expect(handlePrint).toHaveBeenCalled();
});
});
function renderPrintIdentifierSticker() {
render(<PrintIdentifierSticker patient={mockPatient} closeModal={mockedCloseModal} />);
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,14 @@ import {
PatientBannerPatientInfo,
PatientBannerToggleContactDetailsButton,
PatientPhoto,
showModal,
useConfig,
} from '@openmrs/esm-framework';
import { Printer } from '@carbon/react/icons';
import { useTranslation } from 'react-i18next';
import { Button } from '@carbon/react';
import styles from './patient-banner.scss';
import { type ConfigObject } from '../config-schema';

interface PatientBannerProps {
patient: fhir.Patient;
Expand All @@ -17,6 +23,7 @@ interface PatientBannerProps {
}

const PatientBanner: React.FC<PatientBannerProps> = ({ patient, patientUuid, hideActionsOverflow }) => {
const { t } = useTranslation();
const patientBannerRef = useRef(null);
const [isTabletViewport, setIsTabletViewport] = useState(false);

Expand Down Expand Up @@ -46,6 +53,15 @@ const PatientBanner: React.FC<PatientBannerProps> = ({ patient, patientUuid, hid
const maxDesktopWorkspaceWidthInPx = 520;
const showDetailsButtonBelowHeader = patientBannerRef.current?.scrollWidth <= maxDesktopWorkspaceWidthInPx;

const { showPrintIdentifierStickerButton } = useConfig<ConfigObject>();

const openModal = useCallback(() => {
const dispose = showModal('print-identifier-sticker', {
closeModal: () => dispose(),
patient,
});
}, [patient]);

return (
<header
className={classNames(
Expand All @@ -60,13 +76,26 @@ const PatientBanner: React.FC<PatientBannerProps> = ({ patient, patientUuid, hid
</div>
<PatientBannerPatientInfo patient={patient} />
<div className={styles.buttonCol}>
{!hideActionsOverflow ? (
<PatientBannerActionsMenu
actionsSlotName="patient-actions-slot"
isDeceased={patient.deceasedBoolean}
patientUuid={patientUuid}
/>
) : null}
<div className={styles.buttonRow}>
{showPrintIdentifierStickerButton && (
<Button
kind="ghost"
hasIconOnly={true}
renderIcon={(props) => <Printer size={16} {...props} />}
iconDescription={t('printIdentifierSticker', 'Print Identification Sticker')}
tooltipPosition="bottom"
className={styles.printButton}
onClick={openModal}
/>
)}
{!hideActionsOverflow ? (
<PatientBannerActionsMenu
actionsSlotName="patient-actions-slot"
isDeceased={patient.deceasedBoolean}
patientUuid={patientUuid}
/>
) : null}
</div>
{!showDetailsButtonBelowHeader ? (
<PatientBannerToggleContactDetailsButton
className={styles.toggleContactDetailsButton}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
background-color: $ui-01;
}

.activePatientContainer,
.activePatientContainer,
.deceasedPatientContainer {
background-color: $ui-01;
}
Expand Down
Loading

0 comments on commit a529522

Please sign in to comment.