-
Notifications
You must be signed in to change notification settings - Fork 248
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
O3-3276: Add support for printing patient identification stickers
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
1 parent
cb9ad24
commit a529522
Showing
9 changed files
with
385 additions
and
12 deletions.
There are no files selected for viewing
198 changes: 198 additions & 0 deletions
198
packages/esm-patient-banner-app/src/banner-tags/print-identifier-sticker.component.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; |
31 changes: 31 additions & 0 deletions
31
packages/esm-patient-banner-app/src/banner-tags/print-identifier-sticker.scss
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} |
70 changes: 70 additions & 0 deletions
70
packages/esm-patient-banner-app/src/banner-tags/print-identifier-sticker.test.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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} />); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.