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

Adds support for printing 🖨️ prescriptions 💊 #8259

Merged
merged 10 commits into from
Aug 8, 2024
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
Loading