Skip to content

Commit

Permalink
Adds support for printing 🖨️ prescriptions 💊 (#8259)
Browse files Browse the repository at this point in the history
* Adds support for printing prescriptions

* Improve print preview layout

* add links to reach the print page

* update title of print output

* Updated prescription print preview as per requirements

* disable print if empty; add titration instructions; improve layout

* remove todo comments :)

* update disable logic

---------

Co-authored-by: Mohammed Nihal <[email protected]>
  • Loading branch information
rithviknishad and nihal467 authored Aug 8, 2024
1 parent caa411d commit b552047
Show file tree
Hide file tree
Showing 8 changed files with 360 additions and 19 deletions.
36 changes: 36 additions & 0 deletions src/CAREUI/misc/PrintPreview.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { ReactNode } from "react";
import ButtonV2 from "../../Components/Common/components/ButtonV2";
import CareIcon from "../icons/CareIcon";
import { classNames } from "../../Utils/utils";
import Page from "../../Components/Common/components/Page";

type Props = {
children: ReactNode;
disabled?: boolean;
className?: string;
title: string;
};

export default function PrintPreview(props: Props) {
return (
<Page title={props.title}>
<div className="mx-auto my-8 w-[50rem]">
<div className="top-0 z-20 flex justify-end gap-2 bg-secondary-100 px-2 py-4 xl:absolute xl:right-6 xl:top-8">
<ButtonV2 disabled={props.disabled} onClick={() => window.print()}>
<CareIcon icon="l-print" className="text-lg" />
Print
</ButtonV2>
</div>

<div className="bg-white p-10 text-sm shadow-2xl transition-all duration-200 ease-in-out">
<div
id="section-to-print"
className={classNames("w-full", props.className)}
>
{props.children}
</div>
</div>
</div>
</Page>
);
}
10 changes: 9 additions & 1 deletion src/Components/Medicine/ManagePrescriptions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,15 @@ export default function ManagePrescriptions() {
const { goBack } = useAppHistory();

return (
<Page title={t("manage_prescriptions")}>
<Page
title={t("manage_prescriptions")}
options={
<ButtonV2 href="prescriptions/print">
<CareIcon icon="l-print" className="text-lg" />
Print
</ButtonV2>
}
>
<div
className="mx-auto flex w-full max-w-5xl flex-col gap-10 rounded bg-white p-6 transition-all sm:rounded-xl sm:p-12"
id="medicine-preview"
Expand Down
44 changes: 29 additions & 15 deletions src/Components/Medicine/MedicineAdministrationSheet/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,9 @@ const MedicineAdministrationSheet = ({ readonly, is_prn }: Props) => {

const prescriptionList = [
...(data?.results ?? []),
...(showDiscontinued ? discontinuedPrescriptions.data?.results ?? [] : []),
...(showDiscontinued
? (discontinuedPrescriptions.data?.results ?? [])
: []),
];

const { activityTimelineBounds, prescriptions } = useMemo(
Expand Down Expand Up @@ -90,25 +92,37 @@ const MedicineAdministrationSheet = ({ readonly, is_prn }: Props) => {
options={
!readonly &&
!!data?.results && (
<AuthorizedForConsultationRelatedActions>
<>
<AuthorizedForConsultationRelatedActions>
<ButtonV2
id="edit-prescription"
variant="secondary"
border
href="prescriptions"
className="w-full"
>
<CareIcon icon="l-pen" className="text-lg" />
<span className="hidden lg:block">
{t("edit_prescriptions")}
</span>
<span className="block lg:hidden">{t("edit")}</span>
</ButtonV2>
<BulkAdminister
prescriptions={data.results}
onDone={() => refetch()}
/>
</AuthorizedForConsultationRelatedActions>
<ButtonV2
id="edit-prescription"
variant="secondary"
href="prescriptions/print"
ghost
border
href="prescriptions"
disabled={!data.results.length}
className="w-full"
>
<CareIcon icon="l-pen" className="text-lg" />
<span className="hidden lg:block">
{t("edit_prescriptions")}
</span>
<span className="block lg:hidden">{t("edit")}</span>
<CareIcon icon="l-print" className="text-lg" />
Print
</ButtonV2>
<BulkAdminister
prescriptions={data.results}
onDone={() => refetch()}
/>
</AuthorizedForConsultationRelatedActions>
</>
)
}
/>
Expand Down
271 changes: 271 additions & 0 deletions src/Components/Medicine/PrintPreview.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,271 @@
import { useTranslation } from "react-i18next";
import PrintPreview from "../../CAREUI/misc/PrintPreview";
import { useSlugs } from "../../Common/hooks/useSlug";
import routes from "../../Redux/api";
import useQuery from "../../Utils/request/useQuery";
import {
classNames,
formatDate,
formatDateTime,
formatName,
patientAgeInYears,
} from "../../Utils/utils";
import MedicineRoutes from "./routes";
import { Prescription } from "./models";
import useConfig from "../../Common/hooks/useConfig";
import { ReactNode } from "react";

export default function PrescriptionsPrintPreview() {
const { main_logo } = useConfig();
const { t } = useTranslation();
const [patientId, consultationId] = useSlugs("patient", "consultation");

const patientQuery = useQuery(routes.getPatient, {
pathParams: { id: patientId },
});

const encounterQuery = useQuery(routes.getConsultation, {
pathParams: { id: consultationId },
});

const prescriptionsQuery = useQuery(MedicineRoutes.listPrescriptions, {
pathParams: { consultation: consultationId },
query: { discontinued: false, limit: 100 },
});

const patient = patientQuery.data;
const encounter = encounterQuery.data;

const items = prescriptionsQuery.data?.results;
const normalPrescriptions = items?.filter((p) => p.dosage_type !== "PRN");
const prnPrescriptions = items?.filter((p) => p.dosage_type === "PRN");

return (
<PrintPreview
title={
patient ? `Prescriptions - ${patient.name}` : "Print Prescriptions"
}
disabled={!(patient && encounter && items)}
>
<div className="mb-3 flex items-center justify-between p-4">
<h3>{encounter?.facility_name}</h3>
<img className="h-10 w-auto" src={main_logo.dark} alt="care logo" />
</div>
<div className="mb-6 grid grid-cols-8 gap-y-1.5 border-2 border-secondary-400 p-2">
<PatientDetail name="Patient" className="col-span-5">
{patient && (
<>
<span className="uppercase">{patient.name}</span> -{" "}
{t(`GENDER__${patient.gender}`)},{" "}
{patientAgeInYears(patient).toString()}yrs
</>
)}
</PatientDetail>
<PatientDetail name="IP/OP No." className="col-span-3">
{encounter?.patient_no}
</PatientDetail>

<PatientDetail
name={
encounter
? `${t(`encounter_suggestion__${encounter.suggestion}`)} on`
: ""
}
className="col-span-5"
>
{formatDate(encounter?.encounter_date)}
</PatientDetail>
<PatientDetail name="Bed" className="col-span-3">
{encounter?.current_bed?.bed_object.location_object?.name}
{" - "}
{encounter?.current_bed?.bed_object.name}
</PatientDetail>

<PatientDetail name="Allergy to medication" className="col-span-8">
{patient?.allergies ?? "None"}
</PatientDetail>
</div>

<PrescriptionsTable items={normalPrescriptions} />
<PrescriptionsTable items={prnPrescriptions} prn />

<div className="pt-12">
<p className="font-medium text-secondary-800">
Sign of the Consulting Doctor
</p>
<PatientDetail name="Name of the Consulting Doctor">
{encounter?.treating_physician_object &&
formatName(encounter?.treating_physician_object)}
</PatientDetail>
<p className="pt-6 text-center text-xs font-medium text-secondary-700">
Generated on: {formatDateTime(new Date())}
</p>
<p className="pt-1 text-center text-xs font-medium text-secondary-700">
This is a computer generated prescription. It shall be issued to the
patient only after the concerned doctor has verified the content and
authorized the same by affixing signature.
</p>
</div>
</PrintPreview>
);
}

const PatientDetail = ({
name,
children,
className,
}: {
name: string;
children?: ReactNode;
className?: string;
}) => {
return (
<div
className={classNames(
"inline-flex items-center whitespace-nowrap text-sm tracking-wide",
className,
)}
>
<div className="font-medium text-secondary-800">{name}: </div>
{children != null ? (
<span className="pl-2 font-bold">{children}</span>
) : (
<div className="h-5 w-48 animate-pulse bg-secondary-200" />
)}
</div>
);
};

const PrescriptionsTable = ({
items,
prn,
}: {
items?: Prescription[];
prn?: boolean;
}) => {
if (!items) {
return (
<div className="h-96 w-full animate-pulse rounded-lg bg-secondary-200" />
);
}

if (!items.length) {
return;
}

return (
<table className="mb-8 mt-4 w-full border-collapse border-2 border-secondary-400">
<caption className="mb-2 caption-top text-lg font-bold">
{prn && "PRN"} Prescriptions
</caption>
<thead className="border-b-2 border-secondary-400 bg-secondary-50">
<tr>
<th className="max-w-52 p-1">Medicine</th>
<th className="p-1">Dosage</th>
<th className="p-1">Directions</th>
{/* <th className="p-1">{prn ? "Indicator" : "Freq."}</th> */}
<th className="max-w-32 p-1">Notes / Instructions</th>
</tr>
</thead>
<tbody className="border-b-2 border-secondary-400">
{items.map((item) => (
<PrescriptionEntry key={item.id} obj={item} />
))}
</tbody>
</table>
);
};

const PrescriptionEntry = ({ obj }: { obj: Prescription }) => {
const { t } = useTranslation();
const medicine = obj.medicine_object;

return (
<tr className="border-y border-y-secondary-400 text-center text-xs transition-all duration-200 ease-in-out even:bg-secondary-100">
<td className="max-w-52 px-2 py-2 text-start text-sm">
<p>
<strong className="uppercase">
{medicine?.name ?? obj.medicine_old}
</strong>{" "}
</p>
{medicine?.type === "brand" && (
<span className="text-xs text-secondary-600">
<p>
Generic:{" "}
<span className="capitalize text-secondary-800">
{medicine.generic ?? "--"}
</span>
</p>
<p>
Brand:{" "}
<span className="capitalize text-secondary-800">
{medicine.company ?? "--"}
</span>
</p>
</span>
)}
</td>
<td className="space-y-1 px-2 py-1 text-center">
{obj.dosage_type === "TITRATED" && <p>Titrated</p>}
<p className="font-semibold">
{obj.base_dosage}{" "}
{obj.target_dosage != null && `→ ${obj.target_dosage}`}{" "}
</p>
{obj.max_dosage && (
<p>
Max. <span className="font-semibold">{obj.max_dosage}</span> in
24hrs
</p>
)}
{obj.min_hours_between_doses && (
<p>
Min.{" "}
<span className="font-semibold">
{obj.min_hours_between_doses}hrs
</span>{" "}
b/w doses
</p>
)}
</td>
<td className="max-w-32 whitespace-break-spaces px-2 py-1">
{obj.route && (
<p>
<span className="text-secondary-700">Route: </span>
<span className="font-medium">
{t(`PRESCRIPTION_ROUTE_${obj.route}`)}
</span>
</p>
)}
{obj.frequency && (
<p>
<span className="text-secondary-700">Freq: </span>
<span className="font-medium">
{t(`PRESCRIPTION_FREQUENCY_${obj.frequency}`)}
</span>
</p>
)}
{obj.days && (
<p>
<span className="text-secondary-700">Days: </span>
<span className="font-medium">{obj.days} day(s)</span>
</p>
)}
{obj.indicator && (
<p>
<span className="text-secondary-700">Indicator: </span>
<span className="font-medium">{obj.indicator}</span>
</p>
)}
</td>
<td className="max-w-36 whitespace-break-spaces break-words px-2 py-1 text-left text-xs">
{obj.notes}
{obj.instruction_on_titration && (
<p className="pt-1">
<span className="text-secondary-700">Titration instructions:</span>{" "}
{obj.instruction_on_titration}
</p>
)}
</td>
</tr>
);
};
5 changes: 4 additions & 1 deletion src/Locale/en/Common.json
Original file line number Diff line number Diff line change
Expand Up @@ -177,5 +177,8 @@
"caution": "Caution",
"feed_optimal_experience_for_phones": "For optimal viewing experience, consider rotating your device.",
"feed_optimal_experience_for_apple_phones": "For optimal viewing experience, consider rotating your device. Ensure auto-rotate is enabled in your device settings.",
"action_irreversible": "This action is irreversible"
"action_irreversible": "This action is irreversible",
"GENDER__1": "Male",
"GENDER__2": "Female",
"GENDER__3": "Non-binary"
}
Loading

0 comments on commit b552047

Please sign in to comment.