From 93151d42f00377be256613a51c306cb79697b91c Mon Sep 17 00:00:00 2001 From: NikhilA8606 Date: Mon, 8 Jun 2026 13:42:45 +0530 Subject: [PATCH] Redesign print preview of drug chart --- public/locale/en.json | 7 + .../PrintMedicationAdministration.tsx | 711 +++++++++++------- 2 files changed, 452 insertions(+), 266 deletions(-) diff --git a/public/locale/en.json b/public/locale/en.json index c925ea88482..3698157dd61 100644 --- a/public/locale/en.json +++ b/public/locale/en.json @@ -601,6 +601,7 @@ "administered_via_method": "via {{method}}", "administration_dosage_range_error": "Dosage should be between start and target dosage", "administration_notes": "Administration Notes", + "administration_times": "Administration Times", "administrator": "Administrator", "admission_source": "Admission Source", "admit_source": "Admit Source", @@ -612,6 +613,7 @@ "advanced_filters": "Advanced Filters", "after": "after", "after_adjustments": "After Adjustments", + "afternoon_time": "A-Afternoon", "age": "Age", "age_input_warning": "While entering a patient's age is an option, please note that only the year of birth will be captured from this information.", "age_input_warning_bold": "Recommended only when the patient's date of birth is unknown", @@ -977,6 +979,7 @@ "bed_planned_cancelled": "Planned bed assignment has been cancelled.", "bed_requires_parent_location": "Beds can only be created under a parent location", "bed_search_placeholder": "Search by beds name", + "bed_time": "B-Bedtime", "bed_type": "Bed Type", "bed_type__100": "ICU Bed", "bed_type__200": "Ordinary Bed", @@ -2809,6 +2812,7 @@ "home_facility_updated_error": "Error while updating Home Facility", "home_facility_updated_success": "Home Facility updated successfully", "home_health": "Home Health", + "hospital_id": "Hospital ID", "hospital_identifier": "Hospital Identifier", "hospitalisation": "Hospitalisation", "hospitalisation_details": "Hospitalization Details", @@ -3513,6 +3517,7 @@ "more_info": "More Info", "more_options": "More options", "morning_slots": "Morning Slots", + "morning_time": "M-Morning", "mov_file_not_supported": "MOV file not supported in this browser", "mov_file_safari_only": "MOV files are only supported in Safari browser. Please use Safari to view this file or download it to play locally.", "move": "Move", @@ -3593,6 +3598,7 @@ "next_sessions": "Next Sessions", "next_week": "Next Week", "next_week_short": "Next wk", + "night_time": "N-Night", "no": "No", "no_account_found": "No account found", "no_accounts_found": "No accounts found", @@ -4249,6 +4255,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": "Log in as Patient", diff --git a/src/components/Medicine/MedicationAdministration/PrintMedicationAdministration.tsx b/src/components/Medicine/MedicationAdministration/PrintMedicationAdministration.tsx index dc137e91ba1..cf1e41448ec 100644 --- a/src/components/Medicine/MedicationAdministration/PrintMedicationAdministration.tsx +++ b/src/components/Medicine/MedicationAdministration/PrintMedicationAdministration.tsx @@ -1,16 +1,15 @@ import careConfig from "@careConfig"; import { useQuery } from "@tanstack/react-query"; import { format } from "date-fns"; -import { useMemo, useState } from "react"; -import { useTranslation } from "react-i18next"; +import { Bed } from "lucide-react"; +import { Fragment, useMemo, useState } from "react"; +import { Trans, useTranslation } from "react-i18next"; import { cn } from "@/lib/utils"; import PrintPreview from "@/CAREUI/misc/PrintPreview"; -import { Button } from "@/components/ui/button"; import { Checkbox } from "@/components/ui/checkbox"; -import { Label } from "@/components/ui/label"; import { Popover, PopoverContent, @@ -19,7 +18,11 @@ import { import Loading from "@/components/Common/Loading"; import PrintFooter from "@/components/Common/PrintFooter"; -import { formatDosage, formatFrequency } from "@/components/Medicine/utils"; +import { + formatDosage, + formatDuration, + formatFrequency, +} from "@/components/Medicine/utils"; import useCurrentFacilitySilently from "@/pages/Facility/utils/useCurrentFacility"; import encounterApi from "@/types/emr/encounter/encounterApi"; @@ -31,23 +34,27 @@ import { } from "@/types/emr/medicationRequest/medicationRequest"; import medicationRequestApi from "@/types/emr/medicationRequest/medicationRequestApi"; import { PrintTemplateType } from "@/types/facility/printTemplate"; +import { LocationRead } from "@/types/location/location"; +import { PatientIdentifierUse } from "@/types/patient/patientIdentifierConfig/patientIdentifierConfig"; import query from "@/Utils/request/query"; import { formatName, formatPatientAge } from "@/Utils/utils"; -// Generate time slots based on count per day -const generateTimeSlots = (count: number) => { - const hoursPerSlot = 24 / count; - const slots = []; - for (let i = 0; i < count; i++) { - const start = i * hoursPerSlot; - const end = (i + 1) * hoursPerSlot; - slots.push({ - label: `${String(start).padStart(2, "0")}:00`, - start, - end: end === 24 ? 24 : end, - }); - } - return slots; +// The four administration time slots (Bedtime / Morning / Afternoon / Night). +// Each carries its own abbreviation so the chart label stays correct even when +// some slots are filtered out. +const ADMIN_TIME_SLOTS = [ + { key: "B", i18nKey: "bed_time", start: 0, end: 6 }, + { key: "M", i18nKey: "morning_time", start: 6, end: 12 }, + { key: "A", i18nKey: "afternoon_time", start: 12, end: 18 }, + { key: "N", i18nKey: "night_time", start: 18, end: 24 }, +] as const; + +// Format a 0-24 hour into a readable 12-hour label e.g. 0 -> "12 AM", 18 -> "06 PM". +const formatSlotHour = (hour: number) => { + const normalized = hour % 24; + const period = normalized < 12 ? "AM" : "PM"; + const display = normalized % 12 === 0 ? 12 : normalized % 12; + return `${String(display).padStart(2, "0")} ${period}`; }; interface GroupedMedication { @@ -104,13 +111,24 @@ export const PrintMedicationAdministration = (props: { }); // Filter state - const [showDiscontinued, setShowDiscontinued] = useState(false); - const [slotsPerDay, setSlotsPerDay] = useState(4); + const [showDiscontinued] = useState(false); + const [selectedSlotKeys, setSelectedSlotKeys] = useState( + ADMIN_TIME_SLOTS.map((slot) => slot.key), + ); - // Generate time slots based on selection + // Build the active time slots from the user's selection, preserving each + // slot's fixed abbreviation and chronological order. const timeSlots = useMemo( - () => generateTimeSlots(slotsPerDay), - [slotsPerDay], + () => + ADMIN_TIME_SLOTS.filter((slot) => + selectedSlotKeys.includes(slot.key), + ).map((slot) => ({ + label: `${String(slot.start).padStart(2, "0")}:00`, + abbreviation: slot.key, + start: slot.start, + end: slot.end, + })), + [selectedSlotKeys], ); // Group medications by product - include all medications (active + stopped) @@ -227,6 +245,25 @@ export const PrintMedicationAdministration = (props: { return index; }, [medicationAdministrations, timeSlots]); + // Build the location breadcrumb (root → ... → bed) from the encounter's + // current location by walking up the parent chain. + const locationPath = useMemo(() => { + const chain: LocationRead[] = []; + let loc: LocationRead | undefined = + encounter?.current_location ?? undefined; + while (loc) { + chain.unshift(loc); + loc = loc.parent; + } + return chain; + }, [encounter]); + + const hospitalId = + encounter?.patient.instance_identifiers?.find( + (identifier) => + identifier.config.config.use === PatientIdentifierUse.official, + )?.value ?? encounter?.external_identifier; + const isLoading = requestsLoading || adminsLoading; if (isLoading) return ; @@ -248,6 +285,7 @@ export const PrintMedicationAdministration = (props: { disabled={!hasData} facility={facility} templateSlug={PrintTemplateType.medication_administration} + hideFacilityHeader > {/* Force landscape with tight margins so the wide drug chart fits on a single A4 sheet per week. The parent print container also adds @@ -269,106 +307,179 @@ export const PrintMedicationAdministration = (props: { .mar-thead { display: table-header-group; } } `} - {/* Print Options - hidden when printing */} -
-
- setShowDiscontinued(checked === true)} - /> - -
-
- -
- {[1, 2, 3, 4].map((num) => ( - - ))} -
+ + setSelectedSlotKeys((prev) => + value + ? [...prev, slot.key] + : prev.filter((key) => key !== slot.key), + ) + } + /> + + + }} + /> + + + {formatSlotHour(slot.start)} - {formatSlotHour(slot.end)} + + + + ); + })}
{/* Header */} -
-
-
-

+
+
+
+

{t("medication_administration_record")}

-

- {encounter?.facility?.name} -

+ {facility?.name && ( +

+ {facility.name} +

+ )} + {facility?.address && ( +

+ {facility.address} +

+ )}
Logo
- {/* Patient Info - Simplified */} -
-
-
- {t("patient_name")} -
-
- {encounter?.patient.name} -
-
-
-
- {t("age")} / {t("sex")} -
-
- {encounter?.patient && - `${formatPatientAge(encounter.patient, true)}, ${t(`GENDER__${encounter.patient.gender}`)}`} -
-
-
-
- {t("encounter_date")} + {encounter && ( +
+
+
+ {t("patient")} + + {":"} + {encounter.patient.name} + +
+ {encounter.period?.start && ( +
+ {t("encounter_date")} + + {":"} + {format( + new Date(encounter.period.start), + "dd MMM yyyy, EEEE", + )} + +
+ )}
-
- {encounter?.period?.start && - format(new Date(encounter.period.start), "dd MMM yyyy")} +
+
+ {`${t("age")}/${t("sex")}`} + + {":"} + {`${formatPatientAge(encounter.patient, true)}, ${t( + `GENDER__${encounter.patient.gender}`, + )}`} + +
+ {hospitalId && ( +
+ {t("hospital_id")} + + {":"} + {hospitalId} + +
+ )}
+ {locationPath.length > 0 && ( +
+ + {t("patient_ip_location")}: + + {locationPath.map((loc, idx) => ( + + {idx > 0 && } + {loc.form === "bd" && ( + + )} + {loc.name} + + ))} +
+ )}
-
+ )}
{/* Regular Medications Drug Chart */} {groupedMedications.regular.length > 0 && (
-

- {t("regular_medications")} -

+
+ + {t("regular_medications")} + +
+ , + }} + /> + ,{" "} + , + }} + /> + ,{" "} + , + }} + /> + ,{" "} + , + }} + /> +
+
{dateRanges.map((dates, idx) => (
0 && "mt-3 print:break-before-page")} > -
- {format(dates[0], "dd MMM yyyy")} –{" "} - {format(dates[dates.length - 1], "dd MMM yyyy")} -
0 && (
-

- {t("prn_medications")} ({t("as_needed")}) -

+
+ + {t("prn_medications")} ({t("as_needed")}) + +
{dateRanges.map((dates, idx) => (
0 && "mt-3 print:break-before-page")} > -
- {format(dates[0], "dd MMM yyyy")} –{" "} - {format(dates[dates.length - 1], "dd MMM yyyy")} -
{ + const instructions = new Set(); + const notes = new Set(); + + group.requests.forEach((request) => { + request.dosage_instruction.forEach((di) => { + const patientInstruction = di.patient_instruction?.trim(); + if (patientInstruction) instructions.add(patientInstruction); + di.additional_instruction?.forEach((ai) => { + const text = ai.display?.trim(); + if (text) instructions.add(text); + }); + }); + const note = request.note?.trim(); + if (note) notes.add(note); + }); + + return { instructions: [...instructions], notes: [...notes] }; +}; + // Drug Chart Table Component const DrugChartTable = ({ groups, @@ -430,41 +561,53 @@ const DrugChartTable = ({ string, Record> >; - timeSlots: { label: string; start: number; end: number }[]; + timeSlots: { + label: string; + abbreviation: string; + start: number; + end: number; + }[]; isPRN?: boolean; }) => { const { t } = useTranslation(); - // Check if this is the last slot of the day (needs thicker border) + const slotLabels = timeSlots.map((slot) => slot.abbreviation); + + // Thicker divider after the final slot of a day; zebra shade alternate slots. const isLastSlotOfDay = (slotIndex: number) => slotIndex === timeSlots.length - 1; + const isShadedSlot = (slotIndex: number) => slotIndex % 2 === 1; + + const totalColumns = 2 + dates.length * timeSlots.length; return ( -
- - - +
+
+ + + {dates.map((date, dateIdx) => ( ))} @@ -473,187 +616,223 @@ const DrugChartTable = ({ timeSlots.map((slot, slotIdx) => ( )), )} - {groups.map((group) => { + {groups.map((group, groupIdx) => { // Get the latest active request for display const latestRequest = group.requests[0]; const instructions = latestRequest.dosage_instruction; + const form = + latestRequest.requested_product?.definitional?.dosage_form + ?.display; + const { instructions: patientInstructions, notes } = + collectInstructionsAndNotes(group); + const hasFooter = + patientInstructions.length > 0 || notes.length > 0; return ( - - + + - {dates.map((date, dateIdx) => - timeSlots.map((slot, slotIdx) => { - const dateKey = format(date, "yyyy-MM-dd"); - // Check all requests in this group for administrations - const admins = group.requests.flatMap( - (req) => - adminIndex[req.id]?.[dateKey]?.[slot.label] || [], - ); - - const hasAdmins = admins.length > 0; - - // Show checkmarks for each administration (up to 3, then show count) - const cellContent = hasAdmins ? ( -
- {admins.length <= 3 ? ( - // Show individual checkmarks + {form &&
{form}
} +
+ {instructions.map((di, idx) => { + const summary = [ + formatDosage(di), + isPRN ? t("as_needed") : formatFrequency(di), + formatDuration(di), + di.method?.display, + ] + .filter(Boolean) + .join(" · "); + return summary ? ( +
+ {summary} +
+ ) : null; + })} +
+ {group.requests.length > 1 && ( +
+ ({group.requests.length} {t("orders")}) +
+ )} + + {dates.map((date, dateIdx) => + timeSlots.map((slot, slotIdx) => { + const dateKey = format(date, "yyyy-MM-dd"); + // Check all requests in this group for administrations + const admins = group.requests.flatMap( + (req) => + adminIndex[req.id]?.[dateKey]?.[slot.label] || [], + ); + + const hasAdmins = admins.length > 0; + const shaded = isShadedSlot(slotIdx); + + // Checkmark per administration (up to 3, then show count) + const cellContent = hasAdmins ? ( + admins.length <= 3 ? (
{admins.map((admin) => ( ))}
) : ( - // Show count with checkmark -
- +
+ - + ×{admins.length}
- )} -
- ) : null; - - return ( -
- ); - }), +
+ {t("by")}{" "} + {formatName(admin.created_by)} +
+ {admin.note && ( +
+ {admin.note} +
+ )} + + ))} + + + + + ) : ( + · + )} + + ); + }), + )} + + {hasFooter && ( + + + )} - + ); })}
+ # + {t("medication")} -
{format(date, "EEE")}
-
- {format(date, "dd/MM")} -
+
{format(date, "EEE")}
+
{format(date, "dd/MM")}
- {slot.label.slice(0, 2)} + {slotLabels[slotIdx]}
-
- {group.productName} -
- {instructions.map((di, idx) => { - const doseText = formatDosage(di); - const routeText = di.route?.display; - const frequencyText = isPRN - ? t("as_needed") - : formatFrequency(di); - const summary = [doseText, routeText, frequencyText] - .filter(Boolean) - .join(" · "); - return summary ? ( -
0 && - "mt-0.5 pt-0.5 border-t border-dashed border-gray-300", - )} - > - {summary} -
- ) : null; - })} - {group.requests.length > 1 && ( -
- ({group.requests.length} {t("orders")}) + +
+ {groupIdx + 1} + +
+ {group.productName}
- )} -
- {hasAdmins ? ( - <> - {/* Print version - simple content without popover */} -
- {cellContent} -
- {/* Screen version - with popover for details */} - - - + + - {cellContent} - - - -
-
- {group.productName} -
-
- {format(date, "EEE, dd MMM")} · {slot.label} +
+
+ {group.productName} +
+
+ {format(date, "EEE, dd MMM")} ·{" "} + {slot.label} +
-
-
- {admins.map((admin, idx) => ( -
-
- - {format( - new Date( - admin.occurrence_period_start, - ), - "HH:mm", - )} - - - {admin.dosage?.dose?.value}{" "} - {admin.dosage?.dose?.unit?.display} - -
-
- {t("by")} {formatName(admin.created_by)} -
- {admin.note && ( -
- {admin.note} +
+ {admins.map((admin, idx) => ( +
+
+ + {format( + new Date( + admin.occurrence_period_start, + ), + "HH:mm", + )} + + + {admin.dosage?.dose?.value}{" "} + {admin.dosage?.dose?.unit?.display} +
- )} -
- ))} -
- - - - ) : ( - · - )} -
+ + {patientInstructions.length > 0 && ( + + {t("instructions")}: + + {patientInstructions.join("; ")} + {" "} + + )} + {patientInstructions.length > 0 && notes.length > 0 && ( + | + )} + {notes.length > 0 && ( + + {t("note")}:{" "} + + {notes.join("; ")} + + + )} +