Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

(feat) O3-3521: Add configurable ability to print multiple stickers on the same page #1934

Open
wants to merge 27 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
d002631
update printing multiple stickers
jnsereko Oct 23, 2024
7be590e
add translations
jnsereko Oct 23, 2024
164de50
fix breaking tests
jnsereko Oct 23, 2024
948b38e
add toogle to printing multiple stickers
jnsereko Oct 24, 2024
8fb8c70
display patient details with grid
jnsereko Oct 31, 2024
496e474
clean code
jnsereko Nov 11, 2024
6f61e2a
add translations
jnsereko Nov 11, 2024
9721a2d
clean more code
jnsereko Nov 11, 2024
9c97742
standardise css
jnsereko Nov 11, 2024
f77d185
enforce overflow style to support printing multiple pages
jnsereko Nov 11, 2024
784183e
improve tests
jnsereko Nov 11, 2024
2f639a1
improve tests
jnsereko Nov 11, 2024
7edc7b6
improve new print window variables
jnsereko Nov 11, 2024
eadf109
Fixup
denniskigen Nov 12, 2024
c2bc395
add other more configurations to allow rendering fields as tables
jnsereko Nov 13, 2024
e182509
fix tests
jnsereko Nov 13, 2024
0ffcb30
fix baselines and add more vertical space between the barcode and pat…
jnsereko Nov 13, 2024
df959ff
show barcode and logo on the same line
jnsereko Nov 13, 2024
5d26a2e
add mistakenly removed header line divider
jnsereko Nov 13, 2024
076b51d
add separator config and reduce the sticker header to patient details…
jnsereko Nov 13, 2024
e2130b4
fix logo an barcode occupying the same space on firefox
jnsereko Nov 14, 2024
ba91b6b
(unclean) save work done on attemptinng to render an svg
jnsereko Nov 20, 2024
2f9d384
(clean) save work done on attemptinng to render an svg
jnsereko Nov 27, 2024
303141a
(chore) Update translations from Transifex (#2108)
github-actions[bot] Nov 20, 2024
b951921
(feat) O3-4071: Improve Start Visit Form to support "Visit Locations"…
mogoodrich Nov 21, 2024
5d6b141
(chore) Update translations from Transifex (#2114)
github-actions[bot] Nov 25, 2024
aa84553
Reduce space between label and value
jnsereko Dec 3, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
"dependencies": {
"@hookform/resolvers": "^3.3.1",
"classnames": "^2.3.2",
"html-to-image": "^1.11.11",
"react-barcode": "^1.5.3",
"react-hook-form": "^7.46.2",
"react-to-print": "^2.14.13",
Expand Down Expand Up @@ -62,7 +63,7 @@
"jest-environment-jsdom": "^29.7.0",
"lint-staged": "^14.0.1",
"lodash": "^4.17.21",
"openmrs": "next",
"openmrs": "^5.8.2-pre.2483",
"prettier": "^3.0.3",
"react": "^18.3.1",
"react-dom": "^18.3.1",
Expand Down
2 changes: 1 addition & 1 deletion packages/esm-form-entry-app/translations/es.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@
"from": "Desde",
"futureDateRestriction": "¡No se permite una fecha futura!",
"hoursAgo": " horas atrás",
"invalidDate": "¡La fecha proporcionada es inválida!",
"invalidDate": "¡La fecha proporcionada no válida!",
"loading": "Cargando...",
"loadingComponent": "Cargando Componente...",
"max": "El valor máximo debe ser {max}",
Expand Down
6 changes: 3 additions & 3 deletions packages/esm-patient-attachments-app/translations/es.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,15 +35,15 @@
"gridView": "Vista de cuadrícula",
"image": "Imagen",
"imageDescription": "Descripción de la imagen",
"imagePlaceholder": "Marcador de posición de la imagen",
"imagePlaceholder": "Marcador de posición de imagen",
"imagePreview": "Vista previa de la imagen",
"name": "nombre",
"nameIsRequired": "El nombre es requerido",
"nameIsRequired": "El nombre es obligatorio",
"noAttachmentsToDisplay": "No hay archivos adjuntos para mostrar para este paciente",
"noImageToDisplay": "No hay imagen para mostrar",
"options": "Opciones",
"successfullyDeleted": "eliminado exitosamente",
"supportedFiletypes": "Los archivos compatibles son {{supportedFiles}}",
"supportedFiletypes": "Los archivos soportados son {{supportedFiles}}",
"tableView": "Vista de tabla",
"type": "Tipo",
"unsupportedFileType": "Tipo de archivo no compatible",
Expand Down
91 changes: 86 additions & 5 deletions packages/esm-patient-banner-app/src/config-schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,17 +34,78 @@ export const configSchema = {
logo: '',
},
},
fields: {
_type: Type.Array,
_description: 'Patient demographics to include in the patient sticker printout',
_default: ['name', 'dob', 'gender', 'identifier', 'age', 'contact', 'address'],
printStickerFields: {
_type: Type.Object,
_description: 'Configuration of the patient sticker fields for the patient identifier stickers',
fields: {
_type: Type.Array,
_description: 'Patient demographics to include in the patient sticker printout',
},
fieldSeparator: {
_type: Type.Boolean,
_description: 'Whether to display a colon symbol alongside each field label',
},
fieldsTableGroups: {
_type: Type.Array,
_description:
'Groups of patient demographic fields to be displayed as distinct tables in the patient sticker. Each group contains a set of related fields that will appear together in one table ie a single line.',
},
fieldsContainerStyleOverrides: {
_type: Type.Object,
_description: 'CSS style elements override how fields appear in the field container.',
},
_default: {
fields: ['name', 'dob', 'gender', 'identifier', 'age', 'contact', 'address'],
fieldSeparator: false,
fieldsTableGroups: [],
fieldsContainerStyleOverrides: {},
},
},
pageSize: {
_type: Type.String,
_description:
'Specifies the paper size for printing the sticker. You can define the size using units (e.g., mm, in) or named sizes (e.g., "148mm 210mm", "A1", "A2", "A4", "A5").',
_default: 'A4',
},
printMultipleStickers: {
jnsereko marked this conversation as resolved.
Show resolved Hide resolved
_type: Type.Object,
_description: 'Configuration of how many stickers to print, together with the columns and rows to print per page',
numberOfStickers: {
_type: Type.Number,
_description: 'The number of patient ID stickers to print',
},
stickerColumnsPerPage: {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I sort of feel that columns per page makes sense, but rows per page should be calculated based on number of columns, size of each sticker, and size of the page

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree with you @ibacher. I also wanted to implement it that way.
The main reason why i chose to use this alternative is to give a user control of where they want to insert a page-break so that sticker content doesn't overflow to the next page. For example if a user specifies 2 rows per page, the page break with be inserted after the second row.

Below are the reasons why i resorted to this.

  • page size has a variety of possibilities - known page sizes like A0, A1, A4, A5, A6, legal, etc needed to be mapped to corresponding length and width. Also page sizes might vary in different printers.
  • The sticker size is dynamic depending on the number of patient fields to display. It might become shorter or longer.
  • Diversity of printers to be used VS different sizes used in different implementations.

Creating a function that could cater for all the use-cases was seemingly complex compared to merely allow a user to explore what works for him/her.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am happy to fix this in a future PR

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's discuss this a bit further. In my view, the main case for printing labels is generally not to print them on standard pieces of paper, but I may be wrong about that. Usually, I'd expect we want to print literal stickers on a label printer, in which case, in would be roughly one label per "page", with each page size being the size of the sticker.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, FWIW, the allowed values here are extremely limited. See MDN or the formal spec.

Copy link
Contributor Author

@jnsereko jnsereko Nov 13, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

in would be roughly one label per "page", with each page size being the size of the sticker.

@ibacher this serves one use-case. If its a thermal printer, then yes we are printing one label for page but with regular printers, we shall be printing multiple labels per page
You can also follow this slack thread about the requirement.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have created a ticket to follow this up

_type: Type.Number,
_description: 'The number of columns of patient ID stickers to print per page',
},
stickerRowsPerPage: {
_type: Type.Number,
_description: 'The number of rows of patient ID stickers to print per page',
},
_default: {
enabled: false,
numberOfStickers: 1,
stickerColumnsPerPage: 1,
stickerRowsPerPage: 1,
},
},
stickerSize: {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

While challenging, for printing purposes, I think the natural units to use here are actually inches or centimetres. Both pixels and rems are screen-display units where as printing-wise, we want to express the overall size of a label on a page of paper.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

auto is fine

_type: Type.Object,
_description: 'Configuration of the patient sticker height and width for the patient identifier stickers',
height: {
_type: Type.String,
_description:
'Specifies the height of each patient ID sticker in the printout in units such as inches or centimetres.',
},
width: {
_type: Type.String,
_description: 'The width of each patient ID sticker in the printout in units such as inches or centimetres.',
},
_default: {
height: 'auto',
width: 'auto',
},
},
identifiersToDisplay: {
_type: Type.Array,
_description:
Expand All @@ -54,6 +115,11 @@ export const configSchema = {
_type: Type.UUID,
},
},
autoPrint: {
_type: Type.Boolean,
_description: 'Whether to print the patient sticker by default',
_default: false,
},
},
useRelationshipNameLink: {
_type: Type.Boolean,
Expand All @@ -72,9 +138,24 @@ export interface ConfigObject {
showLogo: boolean;
logo: string;
};
fields: Array<AllowedPatientFields>;
printStickerFields: {
fields: Array<AllowedPatientFields>;
fieldSeparator: boolean;
fieldsTableGroups: Array<Array<AllowedPatientFields>>;
fieldsContainerStyleOverrides: Record<string, string | number>;
};
pageSize: string;
printMultipleStickers: {
numberOfStickers: number;
stickerColumnsPerPage: number;
stickerRowsPerPage: number;
};
stickerSize: {
height: string;
width: string;
};
identifiersToDisplay: Array<string>;
autoPrint: boolean;
};
useRelationshipNameLink: boolean;
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,45 +7,48 @@ import styles from './print-identifier-sticker.scss';

export interface PatientDetailProps {
patient: fhir.Patient;
showFieldSeparator: boolean;
}

export const PatientName: React.FC<PatientDetailProps> = ({ patient }) => {
export const PatientName: React.FC<PatientDetailProps> = ({ patient, showFieldSeparator }) => {
const { t } = useTranslation();
return (
<div>
<span>
<strong className={styles.strong}>{t('patientNameWithSeparator', 'Patient name:')}</strong>
<div className={styles.fieldRow}>
<span className={styles.patientDetailLabel}>
{showFieldSeparator ? t('patientNameWithSeparator', 'Patient name:') : t('patientName', 'Patient name')}
</span>
<span className={styles.patientDetail}>{getPatientName(patient)}</span>
</div>
);
};

export const PatientAge: React.FC<PatientDetailProps> = ({ patient }) => {
export const PatientAge: React.FC<PatientDetailProps> = ({ patient, showFieldSeparator }) => {
const { t } = useTranslation();
return (
<div>
<span>
<strong className={styles.strong}>{t('patientAge', 'Age:')}</strong>
<div className={styles.fieldRow}>
<span className={styles.patientDetailLabel}>
{showFieldSeparator ? t('patientAgeWithSeparator', 'Age:') : t('patientAge', 'Age')}
</span>
<span className={styles.patientDetail}>{age(patient.birthDate)}</span>
</div>
);
};

export const PatientDob: React.FC<PatientDetailProps> = ({ patient }) => {
export const PatientDob: React.FC<PatientDetailProps> = ({ patient, showFieldSeparator }) => {
const { t } = useTranslation();
return (
<div>
<span>
<strong className={styles.strong}>{t('patientDateOfBirthWithSeparator', 'Date of birth:')}</strong>
<div className={styles.fieldRow}>
<span className={styles.patientDetailLabel}>
{showFieldSeparator
? t('patientDateOfBirthWithSeparator', 'Date of birth:')
: t('patientDateOfBirth', 'Date of birth')}
</span>
<span className={styles.patientDetail}>{dayjs(patient.birthDate).format('DD-MM-YYYY')}</span>
</div>
);
};

export const PatientGender: React.FC<PatientDetailProps> = ({ patient }) => {
export const PatientGender: React.FC<PatientDetailProps> = ({ patient, showFieldSeparator }) => {
const { t } = useTranslation();
const getGender = (gender: string): string => {
switch (gender) {
Expand All @@ -62,28 +65,29 @@ export const PatientGender: React.FC<PatientDetailProps> = ({ patient }) => {
}
};
return (
<div>
<span>
<strong className={styles.strong}>{t('patientGenderWithSeparator', 'Gender:')}</strong>
<div className={styles.fieldRow}>
<span className={styles.patientDetailLabel}>
{showFieldSeparator ? t('patientGenderWithSeparator', 'Gender:') : t('patientGender', 'Gender')}
</span>
<span className={styles.patientDetail}>{getGender(patient.gender)}</span>
</div>
);
};

export const PatientIdentifier: React.FC<PatientDetailProps> = ({ patient }) => {
export const PatientIdentifier: React.FC<PatientDetailProps> = ({ patient, showFieldSeparator }) => {
const { printPatientSticker } = useConfig<ConfigObject>();
const { identifiersToDisplay } = printPatientSticker ?? {};
const patientIdentifiers =
(identifiersToDisplay ?? []).length === 0
? patient.identifier
: patient.identifier?.filter((identifier) => identifiersToDisplay.includes(identifier.type.coding[0].code));
return (
<div>
<div className={styles.fieldRow}>
{patientIdentifiers?.map((identifier) => (
<div key={identifier.id}>
<span>
<strong className={styles.strong}>{identifier.type.text}:</strong>
<div key={identifier.id} className={styles.fieldRow}>
<span className={styles.patientDetailLabel}>
{identifier.type.text}
{showFieldSeparator ? ':' : ''}
</span>
<span className={styles.patientDetail}>{identifier.value}</span>
</div>
Expand All @@ -92,24 +96,26 @@ export const PatientIdentifier: React.FC<PatientDetailProps> = ({ patient }) =>
);
};

export const PatientContact: React.FC<PatientDetailProps> = ({ patient }) => {
export const PatientContact: React.FC<PatientDetailProps> = ({ patient, showFieldSeparator }) => {
const { t } = useTranslation();

if (!patient?.telecom?.length) {
return null;
}

return (
<div>
<span>
<strong className={styles.strong}>{t('telephoneNumberWithSeparator', 'Telephone number:')}</strong>
<div className={styles.fieldRow}>
<span className={styles.patientDetailLabel}>
{showFieldSeparator
? t('telephoneNumberWithSeparator', 'Telephone number:')
: t('telephoneNumber', 'Telephone number')}
</span>
<span className={styles.patientDetail}>{patient.telecom?.[0]?.value}</span>
</div>
);
};

export const PatientAddress: React.FC<PatientDetailProps> = ({ patient }) => {
export const PatientAddress: React.FC<PatientDetailProps> = ({ patient, showFieldSeparator }) => {
const address = patient?.address?.find((a) => a.use === 'home');
const getAddressKey = (url: string) => url.split('#')[1];

Expand All @@ -121,20 +127,23 @@ export const PatientAddress: React.FC<PatientDetailProps> = ({ patient }) => {
.map(([key, value]) =>
key === 'extension' ? (
address.extension?.[0]?.extension?.map((add, i) => (
<div key={`address-${key}-${i}`}>
<span className={styles.strong}>
<div key={`address-${key}-${i}`} className={styles.fieldRow}>
<span className={styles.patientDetailLabel}>
{getCoreTranslation(
getAddressKey(add.url) as CoreTranslationKey,
getAddressKey(add.url) as CoreTranslationKey,
)}
:
{showFieldSeparator ? ':' : ''}
</span>
<span className={styles.patientDetail}>{add.valueString}</span>
</div>
))
) : (
<div key={`address-${key}`}>
<span className={styles.strong}>{getCoreTranslation(key as CoreTranslationKey, key)}:</span>
<div key={`address-${key}`} className={styles.fieldRow}>
<span className={styles.patientDetailLabel}>
{getCoreTranslation(key as CoreTranslationKey, key)}
{showFieldSeparator ? ':' : ''}
</span>
<span className={styles.patientDetail}>{value}</span>
</div>
),
Expand Down
Loading
Loading