diff --git a/public/locale/en.json b/public/locale/en.json index 2f209390b3a..3ecaa83c591 100644 --- a/public/locale/en.json +++ b/public/locale/en.json @@ -1,5 +1,4 @@ { - "#": "#", "2FA_backup_code": "2FA Backup Code", "404_message": "This page doesn't exist or may have been moved. Please check the URL and try again.", "APPETITE__CANNOT_BE_ASSESSED": "Cannot be assessed", @@ -2795,6 +2794,7 @@ "has_note": "Has Note", "has_sari": "Has SARI (Severe Acute Respiratory illness)?", "has_sub_departments": "Has sub-departments", + "hash_tag": "#", "header": "Header", "header_description": "Configure header elements and layout", "header_error": "There are errors in the Header tab. Please fix the errors before saving.", @@ -3494,6 +3494,7 @@ "medicine_administration": "Medicine Administration", "medicine_administration_history": "Medicine Administration History", "medicine_dispensed": "Medicine Dispensed", + "medicine_instructions": "Medicine Instructions", "medicine_prescription": "Medicine Prescription", "medicines": "Medicines", "medicines_administered": "Medicine(s) administered", @@ -4323,6 +4324,7 @@ "patient_identifiers": "Patient Identifiers", "patient_information": "Patient Information", "patient_instruction": "Patient Instruction", + "patient_ip_location": "Patient IP Location", "patient_is_deceased": "Patient is deceased", "patient_location": "Patient Location", "patient_login": "Sign in as Patient", @@ -6768,6 +6770,7 @@ "workflow_progress": "Workflow Progress", "working_status": "Working Status", "x": "X", + "x_of_y": "{{current}} of {{total}}", "year": "Year", "year_of_birth": "Year of Birth", "year_of_birth_format": "Year of birth must be in YYYY format", diff --git a/src/CAREUI/misc/PrintPreview.tsx b/src/CAREUI/misc/PrintPreview.tsx index be2b2fc8f13..8f25a58ea68 100644 --- a/src/CAREUI/misc/PrintPreview.tsx +++ b/src/CAREUI/misc/PrintPreview.tsx @@ -39,6 +39,7 @@ type Props = { facility?: FacilityRead; templateSlug: string; hideFacilityHeader?: boolean; + footer?: ReactNode; }; export default function PrintPreview(props: Props) { @@ -103,6 +104,7 @@ export default function PrintPreview(props: Props) { facility={props.facility} templateSlug={props.templateSlug} hideFacilityHeader={props.hideFacilityHeader} + footer={props.footer} > {props.children} @@ -312,14 +314,21 @@ function FacilityPrintLayout({ facility, children, hideFacilityHeader, + footer, }: { templateSlug?: string; facility?: FacilityRead; children: ReactNode; hideFacilityHeader?: boolean; + footer?: ReactNode; }) { if (!facility) { - return <>{children}; + return ( + <> + {children} + {footer &&
{footer}
} + + ); } const printTemplate = resolvePrintTemplate(facility, templateSlug); @@ -383,6 +392,7 @@ function FacilityPrintLayout({ /> )} + {footer &&
{footer}
} ); } diff --git a/src/components/Common/PrintTable.tsx b/src/components/Common/PrintTable.tsx index b9a03e775da..5e2b951145a 100644 --- a/src/components/Common/PrintTable.tsx +++ b/src/components/Common/PrintTable.tsx @@ -28,7 +28,9 @@ interface GenericTableProps { headers: HeaderRow[]; rows: TableRowType[] | undefined; className?: string; - classNameCell?: string; + cellClassName?: string; + headerClassName?: string; + tableClassName?: string; cellConfig?: Record; renderCell?: ( key: string, @@ -42,13 +44,33 @@ export default function PrintTable({ headers, rows, className, - classNameCell, + cellClassName, + headerClassName, + tableClassName, cellConfig, renderCell, rowClassName, }: GenericTableProps) { const { t } = useTranslation(); + // Pre-compute which cells are covered by a rowspan from a previous row. + // Rows can encode `_span_${key}: "N"` to span N rows for that column. + const skipCells: Record> = {}; + headers.forEach(({ key }) => { + skipCells[key] = new Set(); + }); + rows?.forEach((row, rowIndex) => { + headers.forEach(({ key }) => { + const spanVal = row[`_span_${key}`]; + if (spanVal) { + const span = parseInt(spanVal); + for (let i = 1; i < span; i++) { + skipCells[key].add(rowIndex + i); + } + } + }); + }); + const getCellContent = ( key: string, value: string | undefined, @@ -66,7 +88,12 @@ export default function PrintTable({ }; return ( -
+
@@ -76,6 +103,7 @@ export default function PrintTable({ index == 0 && "first:rounded-l-md", "h-auto py-1 pl-2 pr-2 text-black text-center ", width && `w-${width}`, + headerClassName, )} key={key} > @@ -86,29 +114,58 @@ export default function PrintTable({ {!!rows && - rows.map((row, index) => ( - - {headers.map(({ key }) => ( - { + if (row["_fullspan"]) { + const skippedCount = headers.filter(({ key }) => + skipCells[key]?.has(index), + ).length; + return ( + - {getCellContent(key, row[key], index)} - - ))} - - ))} + + {row["_fullspan"]} + + + ); + } + return ( + + {headers.map(({ key }) => { + if (skipCells[key]?.has(index)) return null; + const spanVal = row[`_span_${key}`]; + const rowSpan = spanVal ? parseInt(spanVal) : undefined; + return ( + + {getCellContent(key, row[key], index)} + + ); + })} + + ); + })}
diff --git a/src/components/Medicine/FormattedDosage.tsx b/src/components/Medicine/FormattedDosage.tsx index 6b064bc3c5f..c850b210970 100644 --- a/src/components/Medicine/FormattedDosage.tsx +++ b/src/components/Medicine/FormattedDosage.tsx @@ -31,7 +31,7 @@ export function FormattedDosage({ return ( diff --git a/src/components/Medicine/utils.ts b/src/components/Medicine/utils.ts index f65db931148..cb9d5aebaa1 100644 --- a/src/components/Medicine/utils.ts +++ b/src/components/Medicine/utils.ts @@ -7,6 +7,20 @@ import { MedicationRequestDosageInstruction, } from "@/types/emr/medicationRequest/medicationRequest"; import { round } from "@/Utils/decimal"; +import Decimal from "decimal.js"; + +/** + * Round to accounting precision for dosage/medication display. + * If the fractional part is all zeros (e.g. "5.00"), the decimal portion is omitted. + */ +export function roundDosage(value: string | number | Decimal): string { + const fixed = round(value); + const [intPart, fracPart] = fixed.split("."); + if (fracPart && /^0+$/.test(fracPart)) { + return intPart; + } + return fixed; +} // Helper function to format dosage in Rx style export function formatDosage(instruction?: MedicationRequestDosageInstruction) { @@ -14,9 +28,9 @@ export function formatDosage(instruction?: MedicationRequestDosageInstruction) { const { dose_range, dose_quantity } = instruction.dose_and_rate; if (dose_range) { - return `${round(dose_range.low.value)} ${dose_range.low.unit.display} -> ${round(dose_range.high.value)} ${dose_range.high.unit.display}`; + return `${roundDosage(dose_range.low.value)} ${dose_range.low.unit.display} -> ${roundDosage(dose_range.high.value)} ${dose_range.high.unit.display}`; } else if (dose_quantity) { - return `${round(dose_quantity.value)} ${dose_quantity.unit.display}`; + return `${roundDosage(dose_quantity.value)} ${dose_quantity.unit.display}`; } return ""; } @@ -34,7 +48,7 @@ export function isNonUnitDose( const { dose_range, dose_quantity } = doseAndRate; if (dose_range) return true; if (dose_quantity?.value == null) return false; - return round(dose_quantity.value) !== round(1); + return roundDosage(dose_quantity.value) !== roundDosage(1); } // Helper function to format dosage instructions in Rx style @@ -62,7 +76,7 @@ export function formatSig(instruction?: MedicationRequestDosageInstruction) { export function formatDoseRange(range?: DoseRange): string { if (!range?.high?.value) return ""; - return `${round(range.low.value)} → ${round(range.high?.value)} ${range.high?.unit?.display}`; + return `${roundDosage(range.low.value)} → ${roundDosage(range.high?.value)} ${range.high?.unit?.display}`; } /** @@ -140,7 +154,7 @@ export function formatTotalUnits( const dose = prnInstruction.dose_and_rate?.dose_quantity?.value; const doseUnit = prnInstruction.dose_and_rate?.dose_quantity?.unit?.display || unitText; - return dose ? `${round(dose)} ${doseUnit} (PRN)` : "PRN"; + return dose ? `${roundDosage(dose)} ${doseUnit} (PRN)` : "PRN"; } // Sum total dose across all instructions @@ -168,5 +182,5 @@ export function formatTotalUnits( if (!hasAnyDose) return ""; - return `${round(String(totalValue))} ${doseUnit}${hasTapered ? " (tapered)" : ""}`; + return `${roundDosage(String(totalValue))} ${doseUnit}${hasTapered ? " (tapered)" : ""}`; } diff --git a/src/components/Prescription/PrescriptionPreview.tsx b/src/components/Prescription/PrescriptionPreview.tsx index 5951998f46a..05030876290 100644 --- a/src/components/Prescription/PrescriptionPreview.tsx +++ b/src/components/Prescription/PrescriptionPreview.tsx @@ -8,7 +8,6 @@ import PrintPreview from "@/CAREUI/misc/PrintPreview"; import { Markdown } from "@/components/ui/markdown"; import Loading from "@/components/Common/Loading"; -import PrintFooter from "@/components/Common/PrintFooter"; import PrintTable from "@/components/Common/PrintTable"; import { formatDosage, @@ -19,11 +18,13 @@ import { import query from "@/Utils/request/query"; import { formatDateTime, formatName, formatPatientAge } from "@/Utils/utils"; +import { cn } from "@/lib/utils"; import useCurrentFacility from "@/pages/Facility/utils/useCurrentFacility"; import { displayMedicationName } from "@/types/emr/medicationRequest/medicationRequest"; import { PrescriptionRead } from "@/types/emr/prescription/prescription"; import prescriptionApi from "@/types/emr/prescription/prescriptionApi"; import { PrintTemplateType } from "@/types/facility/printTemplate"; +import { getLocationPath } from "@/types/location/utils"; import { PatientIdentifierUse } from "@/types/patient/patientIdentifierConfig/patientIdentifierConfig"; export interface DetailRowProps { @@ -34,8 +35,12 @@ export interface DetailRowProps { const PrescriptionContent = ({ prescription, + prescriptionIndex, + totalCount, }: { prescription: PrescriptionRead; + prescriptionIndex: number; + totalCount: number; }) => { const medications = prescription.medications; const { t } = useTranslation(); @@ -43,67 +48,118 @@ const PrescriptionContent = ({ return (
{/* Prescription Symbol */} -
-

{t("℞")}

-

- {formatDateTime(prescription.created_date, "DD/MM/YYYY hh:mm A")} -

+
+
+ {t("℞")} + + {t("medicine_instructions")} + + {totalCount > 1 && ( + + [ + {t("x_of_y", { + current: prescriptionIndex + 1, + total: totalCount, + })} + ] + + )} +
+ + {formatDateTime( + prescription.created_date, + "DD MMM YYYY, ddd, hh:mm A", + )} +
- {/* Medications Table */} - {medications && medications.length > 0 && ( -
-

{t("medicines")}

+
+
+
+
+

+ + {t("prescribed_by")}:{" "} + + + {formatName(prescription.prescribed_by)} + +

+ {prescription.note && ( +
+ {t("note")}: + +
+ )} +
+
+ {/* Medications Table */} + {medications && medications.length > 0 && ( { - const instructions = medication.dosage_instruction; - const isMulti = instructions.length > 1; - return instructions.map((di, idx) => ({ - _groupedRow: - isMulti && idx < instructions.length - 1 ? "true" : undefined, - medicine: idx === 0 ? displayMedicationName(medication) : "", - dosage: formatDosage(di) || "-", - frequency: formatFrequencyWithInstructions(di) || "-", - duration: formatDuration(di) || "-", - instructions: [formatSig(di), idx === 0 ? medication.note : ""] - .filter(Boolean) - .join("\n"), - })); - })} - className="text-sm break-words font-semibold whitespace-break-spaces text-gray-950" + rows={medications.flatMap( + (medication, medIndex): Record[] => { + const instructions = medication.dosage_instruction; + const isMulti = instructions.length > 1; + const totalRows = + instructions.length + (medication.note ? 1 : 0); + const shouldSpan = totalRows > 1; + return [ + ...instructions.map((di, idx) => ({ + _groupedRow: + isMulti && idx < instructions.length - 1 + ? "true" + : undefined, + _span_hash_tag: + idx === 0 && shouldSpan ? String(totalRows) : undefined, + _span_medicine: + idx === 0 && shouldSpan ? String(totalRows) : undefined, + hash_tag: idx === 0 ? String(medIndex + 1) : "", + medicine: + idx === 0 ? displayMedicationName(medication) : "", + dosage: formatDosage(di) || "", + frequency: formatFrequencyWithInstructions(di) || "", + duration: formatDuration(di) || "", + instructions: formatSig(di) || "", + })), + ...(medication.note + ? [ + { + _fullspan: `${t("note")}: ${medication.note}`, + }, + ] + : []), + ]; + }, + )} + cellClassName="text-sm print:text-xs wrap-break-word whitespace-break-spaces text-gray-950 font-normal text-left" cellConfig={{ - medicine: { className: "text-left" }, - frequency: { className: "text-left" }, + hash_tag: { className: "text-center text-gray-600 w-8" }, + medicine: { className: "font-medium max-w-56 min-w-32" }, + dosage: { className: "w-24" }, + duration: { className: "border-r w-20" }, + frequency: { className: "min-w-24 max-w-56" }, + instructions: { className: "min-w-28 max-w-56" }, }} - rowClassName={(row) => (row._groupedRow ? "border-b-0" : undefined)} - /> -
- )} - {prescription?.note && ( -
-

{t("note")}

- -
- )} + )} +
+ {/* Doctor's Signature */} -
-
-

{t("prescribed_by")}

-

- {formatName(prescription.prescribed_by)} -

-
+ +
+

+ {formatName(prescription.prescribed_by)} +

); @@ -111,10 +167,17 @@ const PrescriptionContent = ({ const DetailRow = ({ label, value, isStrong = false }: DetailRowProps) => { return ( -
- {label} - : - +
+ + {label} + + : + {value || "-"}
@@ -195,52 +258,84 @@ export const PrescriptionPreview = ({ disabled={!hasMedications} facility={facility} templateSlug={PrintTemplateType.prescription} + footer={ +
+ {t("computer_generated_prescription")}| + {format(new Date(), "PP 'at' p")} +
+ } >
{/* Patient Details */} -
-
- - - {patient.instance_identifiers - ?.filter( - ({ config }) => - config.config.use === PatientIdentifierUse.official, - ) - .map((identifier) => ( +
+
+
+ {/* Left column: Patient, Age/Sex, Mobile */} +
- ))} - {prescriptions.length === 1 && ( - - )} - + + +
+ + {/* Right column: Identifiers + Encounter Date */} +
+ {patient.instance_identifiers + ?.filter( + ({ config }) => + config.config.use === PatientIdentifierUse.official, + ) + .map((identifier) => ( + + ))} + {prescriptions.length === 1 && ( + + )} +
+
+ + {prescriptions.length === 1 && + prescriptions[0].encounter.current_location && ( + + )}
-
+ +
@@ -248,7 +343,7 @@ export const PrescriptionPreview = ({
{prescriptions.length > 1 && ( -
+
{t("prescriptions_count", { count: prescriptions.length })}
)} @@ -258,15 +353,13 @@ export const PrescriptionPreview = ({ {index > 0 && (
)} - +
))} - - {/* Footer */} -
diff --git a/src/pages/Facility/services/inventory/SupplyDeliveryTable.tsx b/src/pages/Facility/services/inventory/SupplyDeliveryTable.tsx index e48de511eaa..0eb1411f5cc 100644 --- a/src/pages/Facility/services/inventory/SupplyDeliveryTable.tsx +++ b/src/pages/Facility/services/inventory/SupplyDeliveryTable.tsx @@ -187,7 +187,7 @@ export function SupplyDeliveryTable({ )} - {t("#")} + {t("hash_tag")} {t("item")} {t("batch")} {t("requested_qty")} diff --git a/src/pages/Facility/services/pharmacy/PrintDispenseOrder.tsx b/src/pages/Facility/services/pharmacy/PrintDispenseOrder.tsx index 8f589905bb5..1eaf6ba1caa 100644 --- a/src/pages/Facility/services/pharmacy/PrintDispenseOrder.tsx +++ b/src/pages/Facility/services/pharmacy/PrintDispenseOrder.tsx @@ -85,7 +85,7 @@ const DispenseOrderContent = ({ { key: "expiry_date" }, { key: "prepared_date" }, ]} - classNameCell="whitespace-pre-line" + cellClassName="whitespace-pre-line" rows={dispenses.map((dispense) => { const instructions = dispense.dosage_instruction ?? []; diff --git a/tests/facility/patient/encounter/medicine/prescriptionCreate.spec.ts b/tests/facility/patient/encounter/medicine/prescriptionCreate.spec.ts index 5f361a41d0b..029b58177a7 100644 --- a/tests/facility/patient/encounter/medicine/prescriptionCreate.spec.ts +++ b/tests/facility/patient/encounter/medicine/prescriptionCreate.spec.ts @@ -198,9 +198,10 @@ test.describe("Create Patient Prescription", () => { await expect(table).toContainText(medicineName); await expect(table).toContainText(dosage); // Unit dosages (value === 1) are NOT visually highlighted - const medicationRow = table.getByRole("row").filter({ - hasText: medicineName, - }); + const medicationRow = table + .getByRole("row") + .filter({ hasText: medicineName }) + .filter({ hasText: frequency.display }); await expect(medicationRow.locator(".bg-yellow-100")).toHaveCount(0); }); }); diff --git a/tests/facility/patient/encounter/structuredQuestions/diagnosis.spec.ts b/tests/facility/patient/encounter/structuredQuestions/diagnosis.spec.ts index 22bf6617487..73fb7241a43 100644 --- a/tests/facility/patient/encounter/structuredQuestions/diagnosis.spec.ts +++ b/tests/facility/patient/encounter/structuredQuestions/diagnosis.spec.ts @@ -107,6 +107,9 @@ test.describe("Diagnosis", () => { await diagnosisRow.getByRole("cell").nth(4).click(); await page.getByRole("option", { name: verification, exact: true }).click(); + await expect(diagnosisRow.getByRole("cell").nth(4)).toContainText( + verification, + ); await page.getByRole("button", { name: "Submit" }).click(); @@ -191,6 +194,6 @@ test.describe("Diagnosis", () => { await expect(diagnosisRow.getByText("Verification")).toBeVisible(); await expect(diagnosisRow.getByText("Onset")).toBeVisible(); - await expect(diagnosisRow.getByText(diagnosisName)).toBeVisible(); + await expect(diagnosisRow.getByText(diagnosisName).first()).toBeVisible(); }); }); diff --git a/tests/facility/patient/encounter/structuredQuestions/medicationRequest.spec.ts b/tests/facility/patient/encounter/structuredQuestions/medicationRequest.spec.ts index d0353d6cce4..6191954d1ea 100644 --- a/tests/facility/patient/encounter/structuredQuestions/medicationRequest.spec.ts +++ b/tests/facility/patient/encounter/structuredQuestions/medicationRequest.spec.ts @@ -236,7 +236,7 @@ test.describe("Medication Request Questionnaire", () => { const medicationRow = page .locator('[data-slot="table-body"] tr') .filter({ hasText: medicationName }) - .filter({ hasText: `${dosageQuantity.toFixed(2)} ${dosageUnit}` }) + .filter({ hasText: `${dosageQuantity} ${dosageUnit}` }) .filter({ hasText: frequencyData.display }) .filter({ hasText: `${duration} ${durationUnit}` }); @@ -295,7 +295,7 @@ test.describe("Medication Request Questionnaire", () => { const medicationRow = page .locator('[data-slot="table-body"] tr') .filter({ hasText: medicationName }) - .filter({ hasText: `${dosageQuantity.toFixed(2)} ${dosageUnit}` }) + .filter({ hasText: `${dosageQuantity} ${dosageUnit}` }) .filter({ hasText: frequencyData.display }) .filter({ hasText: `${duration} ${durationUnit}` });