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 && (
-
- )}
+ )}
+
+
{/* 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}` });