diff --git a/public/locale/en.json b/public/locale/en.json
index 79525b6771b..fb0e5e3381d 100644
--- a/public/locale/en.json
+++ b/public/locale/en.json
@@ -242,7 +242,7 @@
"SYSTEM__org_type__team": "Team",
"Submit": "Submit",
"TELEMEDICINE": "Telemedicine",
- "TRANSPORTATION TO BE ARRANGED": "Transportation",
+ "TRANSPORTATION_TO_BE_ARRANGED": "Transportation",
"URINATION_FREQUENCY__DECREASED": "Decreased",
"URINATION_FREQUENCY__INCREASED": "Increased",
"URINATION_FREQUENCY__NORMAL": "Normal",
@@ -265,7 +265,7 @@
"VENTILATOR_MODE__VCV_short": "VCV",
"VENTILATOR_MODE__VC_SIMV": "Volume Controlled SIMV (VC-SIMV)",
"VENTILATOR_MODE__VC_SIMV_short": "VC-SIMV",
- "View Facility": "View Facility",
+ "View_Facility": "View Facility",
"aadhaar_number": "Aadhaar Number",
"aadhaar_number_will_not_be_stored": "Aadhaar number will not be stored by CARE",
"aadhaar_otp_send_error": "Failed to send OTP. Please try again later.",
@@ -319,6 +319,7 @@
"add_spoke": "Add Spoke Facility",
"add_tags": "Add Tags",
"add_user": "Add User",
+ "added_on": "Added on",
"additional_information": "Additional Information",
"additional_instructions": "Additional Instructions",
"address": "Address",
@@ -485,6 +486,7 @@
"cancel_appointment": "Cancel Appointment",
"cancel_appointment_warning": "This action cannot be undone. The appointment will be cancelled and the patient will be notified.",
"cancelled": "Cancelled",
+ "cannot_go_before_prescription_date": "Cannot view slots before the earliest prescription date",
"cannot_select_date_out_of_range": "Cannot select date out of range",
"cannot_select_month_out_of_range": "Cannot select month out of range",
"cannot_select_year_out_of_range": "Cannot select year out of range",
@@ -752,12 +754,14 @@
"downloading": "Downloading",
"downloading_abha_card": "Generating ABHA Card, Please hold on",
"downloads": "Downloads",
+ "draft": "Draft",
"drag_drop_image_to_upload": "Drag & drop image to upload",
"duplicate_patient_record_birth_unknown": "Please contact your district care coordinator, the shifting facility or the patient themselves if you are not sure about the patient's year of birth.",
"duplicate_patient_record_confirmation": "Admit the patient record to your facility by adding the year of birth",
"duplicate_patient_record_rejection": "I confirm that the suspect / patient I want to create is not on the list.",
"duration": "Duration",
"edit": "Edit",
+ "edit_administration": "Edit Administration",
"edit_avatar": "Edit Avatar",
"edit_avatar_note": "Change the avatar of the user",
"edit_avatar_note_self": "Change your avatar",
@@ -902,6 +906,9 @@
"end_datetime": "End Date/Time",
"end_dose": "End Dose",
"end_time": "End Time",
+ "end_time_before_start_error": "End time cannot be before start time",
+ "end_time_future_error": "End time cannot be in the future",
+ "ended": "Ended",
"enter_dosage_instructions": "Enter Dosage Instructions",
"enter_file_name": "Enter File Name",
"enter_message": "Start typing...",
@@ -1074,6 +1081,7 @@
"icmr_specimen_referral_form": "ICMR Specimen Referral Form",
"immunisation-records": "Immunisation",
"in_consultation": "In-Consultation",
+ "in_progress": "In Progress",
"inactive": "Inactive",
"incoming": "Incoming",
"incomplete_patient_details_warning": "Patient details are incomplete. Please update the details before proceeding.",
@@ -1140,6 +1148,7 @@
"is_it_upshift": "is it upshift",
"is_phone_a_whatsapp_number": "Is the phone number a WhatsApp number?",
"is_pregnant": "Is pregnant",
+ "is_this_administration_for_a_past_time": "Is this administration for a past time",
"is_this_an_emergency": "Is this an Emergency?",
"is_this_an_emergency_request": "Is this an emergency request?",
"is_this_an_upshift": "Is this an upshift?",
@@ -1255,8 +1264,10 @@
"medical_records": "Medical Records",
"medical_worker": "Medical Worker",
"medication": "Medication",
+ "medication_administration_saved": "Medicine Administration saved",
"medication_taken_between": "Medication Taken Between",
"medicine": "Medicine",
+ "medicine_administration": "Medicine Administration",
"medicine_administration_history": "Medicine Administration History",
"medicine_prescription": "Medicine Prescription",
"medicines_administered": "Medicine(s) administered",
@@ -1314,6 +1325,7 @@
"next_sessions": "Next Sessions",
"next_week_short": "Next wk",
"no": "No",
+ "no_active_medications": "No active medications",
"no_address_provided": "No address provided",
"no_allergies_recorded": "No allergies recorded",
"no_appointments": "No appointments found",
@@ -1350,6 +1362,7 @@
"no_log_updates": "No log updates found",
"no_medical_history_available": "No Medical History Available",
"no_medications_found_for_this_encounter": "No medications found for this encounter.",
+ "no_medications_to_administer": "No medications to administer",
"no_notices_for_you": "No notices for you.",
"no_observations": "No Observations",
"no_ongoing_medications": "No Ongoing Medications",
@@ -1398,6 +1411,7 @@
"none": "None",
"normal": "Normal",
"noshow": "No-show",
+ "not_done": "Not Done",
"not_eligible": "Not Eligible",
"not_found": "Not Found",
"not_specified": "Not Specified",
@@ -1548,6 +1562,7 @@
"phone_number_not_found": "Phone number not found",
"phone_number_validation_error": "Entered phone number is not valid",
"phone_number_verified": "Phone Number Verified",
+ "pick_a_date": "Pick a date",
"pincode": "Pincode",
"pincode_autofill": "State and District auto-filled from Pincode",
"pincode_district_auto_fill_error": "Failed to auto-fill district information",
@@ -1601,6 +1616,7 @@
"practitioner_information": "Practitioner Information",
"preferred_facility_type": "Preferred Facility Type",
"preferred_vehicle": "Preferred Vehicle",
+ "prescribed": "Prescribed",
"prescription": "Prescription",
"prescription_details": "Prescription Details",
"prescription_discontinued": "Prescription discontinued",
@@ -1836,6 +1852,7 @@
"search_investigation_placeholder": "Search Investigation & Groups",
"search_medication": "Search Medication",
"search_medications": "Search for medications to add",
+ "search_medicine": "Search Medicine",
"search_patient_page_text": "Search for existing patients using their phone number or create a new patient record",
"search_patients": "Search Patients",
"search_resource": "Search Resource",
@@ -1935,6 +1952,7 @@
"shifting_details": "Shifting details",
"shifting_history": "Shifting History",
"shifting_status": "Shifting status",
+ "show": "Show",
"show_abha_profile": "Show ABHA Profile",
"show_all": "Show all",
"show_all_notifications": "Show All",
@@ -1976,6 +1994,8 @@
"start_new_clinical_encounter": "Start a new clinical encounter",
"start_review": "Start Review",
"start_time": "Start Time",
+ "start_time_before_authored_error": "Start time cannot be before the medication was prescribed",
+ "start_time_future_error": "Start time cannot be in the future",
"start_time_must_be_before_end_time": "Start time must be before end time",
"state": "State",
"state_reason_for_archiving": "State reason for archiving {{name}} file?",
@@ -2010,6 +2030,7 @@
"tachycardia": "Tachycardia",
"tag_name": "Tag Name",
"tag_slug": "Tag Slug",
+ "taken": "Taken",
"taper_titrate_dosage": "Taper & Titrate Dosage",
"target_dosage": "Target Dosage",
"template_deleted": "Template has been deleted",
diff --git a/src/CAREUI/display/SubHeading.tsx b/src/CAREUI/display/SubHeading.tsx
deleted file mode 100644
index 67072fd9e02..00000000000
--- a/src/CAREUI/display/SubHeading.tsx
+++ /dev/null
@@ -1,34 +0,0 @@
-import { ReactNode } from "react";
-
-import RecordMeta from "@/CAREUI/display/RecordMeta";
-import CareIcon from "@/CAREUI/icons/CareIcon";
-
-interface Props {
- title: ReactNode;
- lastModified?: string;
- className?: string;
- options?: ReactNode;
-}
-
-export default function SubHeading(props: Props) {
- return (
-
-
-
- {props.title}
-
- {props.lastModified && (
-
-
-
-
- )}
-
- {props.options && (
-
- {props.options}
-
- )}
-
- );
-}
diff --git a/src/Utils/request/api.tsx b/src/Utils/request/api.tsx
index 45eddeedb24..c6edaeb3fb6 100644
--- a/src/Utils/request/api.tsx
+++ b/src/Utils/request/api.tsx
@@ -17,6 +17,7 @@ import {
import { PaginatedResponse } from "@/Utils/request/types";
import { AppointmentPatientRegister } from "@/pages/Patient/Utils";
import { Encounter, EncounterEditRequest } from "@/types/emr/encounter";
+import { MedicationAdministration } from "@/types/emr/medicationAdministration/medicationAdministration";
import { MedicationStatement } from "@/types/emr/medicationStatement";
import { PartialPatientModel, Patient } from "@/types/emr/newPatient";
import {
@@ -649,6 +650,14 @@ const routes = {
TRes: Type>(),
},
},
+
+ medicationAdministration: {
+ list: {
+ path: "/api/v1/patient/{patientId}/medication/administration/",
+ method: "GET",
+ TRes: Type>(),
+ },
+ },
} as const;
export default routes;
diff --git a/src/components/Medicine/MedicationAdministration/AdministrationTab.tsx b/src/components/Medicine/MedicationAdministration/AdministrationTab.tsx
new file mode 100644
index 00000000000..39f477f6d65
--- /dev/null
+++ b/src/components/Medicine/MedicationAdministration/AdministrationTab.tsx
@@ -0,0 +1,760 @@
+"use client";
+
+import { useMutation, useQuery } from "@tanstack/react-query";
+import { format, formatDistanceToNow } from "date-fns";
+import { t } from "i18next";
+import React, { useState } from "react";
+
+import CareIcon from "@/CAREUI/icons/CareIcon";
+
+import { Button } from "@/components/ui/button";
+import { Card } from "@/components/ui/card";
+import {
+ Popover,
+ PopoverContent,
+ PopoverTrigger,
+} from "@/components/ui/popover";
+import { ScrollArea, ScrollBar } from "@/components/ui/scroll-area";
+
+import Loading from "@/components/Common/Loading";
+import { EmptyState } from "@/components/Medicine/MedicationRequestTable";
+import { getFrequencyDisplay } from "@/components/Medicine/MedicationsTable";
+import { formatDosage } from "@/components/Medicine/utils";
+
+import routes from "@/Utils/request/api";
+import mutate from "@/Utils/request/mutate";
+import query from "@/Utils/request/query";
+import { formatName } from "@/Utils/utils";
+import {
+ MedicationAdministration,
+ MedicationAdministrationRequest,
+} from "@/types/emr/medicationAdministration/medicationAdministration";
+import { MedicationRequestRead } from "@/types/emr/medicationRequest";
+import medicationRequestApi from "@/types/emr/medicationRequest/medicationRequestApi";
+
+import { MedicineAdminDialog } from "./MedicineAdminDialog";
+import { MedicineAdminSheet } from "./MedicineAdminSheet";
+import {
+ STATUS_COLORS,
+ TIME_SLOTS,
+ createMedicationAdministrationRequest,
+} from "./utils";
+
+const ACTIVE_STATUSES = ["active", "on-hold", "draft", "unknown"] as const;
+const INACTIVE_STATUSES = [
+ "ended",
+ "completed",
+ "cancelled",
+ "entered_in_error",
+] as const;
+
+// Utility Functions
+function isTimeInSlot(
+ date: Date,
+ slot: { date: Date; start: string; end: string },
+): boolean {
+ const slotStartDate = new Date(slot.date);
+ const [startHour] = slot.start.split(":").map(Number);
+ const [endHour] = slot.end.split(":").map(Number);
+
+ slotStartDate.setHours(startHour, 0, 0, 0);
+ const slotEndDate = new Date(slotStartDate);
+ slotEndDate.setHours(endHour, 0, 0, 0);
+
+ return date >= slotStartDate && date < slotEndDate;
+}
+
+function getAdministrationsForTimeSlot(
+ administrations: MedicationAdministration[],
+ medicationId: string,
+ slotDate: Date,
+ start: string,
+ end: string,
+): MedicationAdministration[] {
+ return administrations.filter((admin) => {
+ const adminDate = new Date(admin.occurrence_period_start);
+ const slotStartDate = new Date(slotDate);
+ const slotEndDate = new Date(slotDate);
+
+ const [startHour] = start.split(":").map(Number);
+ const [endHour] = end.split(":").map(Number);
+
+ slotStartDate.setHours(startHour, 0, 0, 0);
+ slotEndDate.setHours(endHour, 0, 0, 0);
+
+ return (
+ admin.request === medicationId &&
+ adminDate >= slotStartDate &&
+ adminDate < slotEndDate
+ );
+ });
+}
+
+// Types and Interfaces
+interface AdministrationTabProps {
+ patientId: string;
+ encounterId: string;
+}
+
+interface TimeSlotHeaderProps {
+ slot: (typeof TIME_SLOTS)[number] & { date: Date };
+ isCurrentSlot: boolean;
+ isEndSlot: boolean;
+}
+
+interface MedicationStatusBadgeProps {
+ status: string;
+}
+
+interface MedicationBadgesProps {
+ medication: MedicationRequestRead;
+}
+
+interface MedicationRowProps {
+ medication: MedicationRequestRead;
+ visibleSlots: ((typeof TIME_SLOTS)[number] & { date: Date })[];
+ currentDate: Date;
+ administrations?: MedicationAdministration[];
+ onAdminister: (medication: MedicationRequestRead) => void;
+ onEditAdministration: (
+ medication: MedicationRequestRead,
+ admin: MedicationAdministration,
+ ) => void;
+ onDiscontinue: (medication: MedicationRequestRead) => void;
+}
+
+// Utility Components
+const MedicationStatusBadge: React.FC = ({
+ status,
+}) => (
+
+ {t(status)}
+
+);
+
+const MedicationBadges: React.FC = ({ medication }) => (
+
+
+ {medication.dosage_instruction[0]?.route?.display || "Oral"}
+
+ {medication.dosage_instruction[0]?.as_needed_boolean && (
+
+ {t("as_needed_prn")}
+
+ )}
+
+
+);
+
+const TimeSlotHeader: React.FC = ({
+ slot,
+ isCurrentSlot,
+ isEndSlot,
+}) => {
+ const isFirstSlotOfDay = slot.start === "00:00";
+ const isLastSlotOfDay = slot.start === "18:00";
+
+ return (
+
+ {isFirstSlotOfDay && (
+
+
+
+ {format(slot.date, "dd MMM").toUpperCase()}
+
+
+ {format(slot.date, "EEE")}
+
+
+
+
+ )}
+ {!isFirstSlotOfDay && !isLastSlotOfDay && (
+
+ )}
+ {isLastSlotOfDay && (
+
+
+
+
+ {format(slot.date, "dd MMM").toUpperCase()}
+
+
+ {format(slot.date, "EEE")}
+
+
+
+ )}
+ {isCurrentSlot && isEndSlot && (
+
+ )}
+
+ );
+};
+
+const MedicationRow: React.FC = ({
+ medication,
+ visibleSlots,
+ currentDate,
+ administrations,
+ onAdminister,
+ onEditAdministration,
+ onDiscontinue,
+}) => {
+ return (
+
+
+
+ {medication.medication?.display}
+
+
+
+ {formatDosage(medication.dosage_instruction[0])},{" "}
+ {
+ getFrequencyDisplay(medication.dosage_instruction[0]?.timing)
+ ?.meaning
+ }
+
+
+ {t("added_on")}:{" "}
+ {format(
+ new Date(medication.authored_on || medication.created_date),
+ "MMM dd, yyyy, hh:mm a",
+ )}
+
+
+
+ {visibleSlots.map((slot) => {
+ const administrationRecords = getAdministrationsForTimeSlot(
+ administrations || [],
+ medication.id,
+ slot.date,
+ slot.start,
+ slot.end,
+ );
+ const isCurrentSlot = isTimeInSlot(currentDate, slot);
+
+ return (
+
+ {administrationRecords?.map((admin) => {
+ const colorClass =
+ STATUS_COLORS[admin.status as keyof typeof STATUS_COLORS] ||
+ STATUS_COLORS.default;
+
+ return (
+
onEditAdministration(medication, admin)}
+ >
+
+
+ {new Date(admin.occurrence_period_start).toLocaleTimeString(
+ "en-US",
+ {
+ hour: "numeric",
+ minute: "2-digit",
+ hour12: true,
+ },
+ )}
+
+ {admin.note && (
+
+
+
+ )}
+
+ );
+ })}
+ {isCurrentSlot && medication.status === "active" && (
+
onAdminister(medication)}
+ >
+ {t("administer")}
+
+ )}
+
+ );
+ })}
+
+
+ {ACTIVE_STATUSES.includes(
+ medication.status as (typeof ACTIVE_STATUSES)[number],
+ ) && (
+
+
+
+
+
+
+
+ {
+ onDiscontinue(medication);
+ // Close the popover after clicking
+ const button = document.activeElement as HTMLElement;
+ button?.blur();
+ }}
+ >
+
+ {t("discontinue")}
+
+
+
+ )}
+
+
+ );
+};
+
+export const AdministrationTab: React.FC = ({
+ patientId,
+ encounterId,
+}) => {
+ const currentDate = new Date();
+ const [endSlotDate, setEndSlotDate] = useState(currentDate);
+ const [showStopped, setShowStopped] = useState(false);
+ const [endSlotIndex, setEndSlotIndex] = useState(
+ Math.floor(currentDate.getHours() / 6),
+ );
+ // Calculate visible slots based on end slot
+ const visibleSlots = React.useMemo(() => {
+ const slots = [];
+ let currentIndex = endSlotIndex;
+ let currentDate = new Date(endSlotDate);
+
+ // Add slots from right to left
+ for (let i = 0; i < 4; i++) {
+ if (currentIndex < 0) {
+ currentIndex = 3;
+ currentDate = new Date(currentDate);
+ currentDate.setDate(currentDate.getDate() - 1);
+ }
+ slots.unshift({
+ ...TIME_SLOTS[currentIndex],
+ date: new Date(currentDate),
+ });
+ currentIndex--;
+ }
+ return slots;
+ }, [endSlotDate, endSlotIndex]);
+
+ // Queries
+ const { data: activeMedications, refetch: refetchActive } = useQuery({
+ queryKey: ["medication_requests_active", patientId],
+ queryFn: query(medicationRequestApi.list, {
+ pathParams: { patientId },
+ queryParams: {
+ encounter: encounterId,
+ limit: 100,
+ status: ACTIVE_STATUSES.join(","),
+ },
+ }),
+ enabled: !!patientId,
+ });
+
+ const { data: stoppedMedications, refetch: refetchStopped } = useQuery({
+ queryKey: ["medication_requests_stopped", patientId],
+ queryFn: query(medicationRequestApi.list, {
+ pathParams: { patientId },
+ queryParams: {
+ encounter: encounterId,
+ limit: 100,
+ status: INACTIVE_STATUSES.join(","),
+ },
+ }),
+ enabled: !!patientId,
+ });
+
+ const { data: administrations, refetch: refetchAdministrations } = useQuery({
+ queryKey: ["medication_administrations", patientId, visibleSlots],
+ queryFn: query(routes.medicationAdministration.list, {
+ pathParams: { patientId },
+ queryParams: {
+ encounter: encounterId,
+ limit: 100,
+ ...(visibleSlots.length > 0 && {
+ occurrence_period_start_after: (() => {
+ const firstSlot = visibleSlots[0];
+ const [startHour] = firstSlot.start.split(":").map(Number);
+ const date = new Date(firstSlot.date);
+ date.setHours(startHour, 0, 0, 0);
+ return format(date, "yyyy-MM-dd'T'HH:mm:ss");
+ })(),
+ occurrence_period_start_before: (() => {
+ const lastSlot = visibleSlots[visibleSlots.length - 1];
+ const [endHour] = lastSlot.end.split(":").map(Number);
+ const date = new Date(lastSlot.date);
+ date.setHours(endHour, 0, 0, 0);
+ return format(date, "yyyy-MM-dd'T'HH:mm:ss");
+ })(),
+ }),
+ },
+ }),
+ enabled: !!patientId && !!visibleSlots?.length,
+ });
+
+ // Get last administered date and last administered by for each medication
+ const lastAdministeredDetails = React.useMemo(() => {
+ return administrations?.results?.reduce<{
+ dates: Record;
+ performers: Record;
+ }>(
+ (acc, admin) => {
+ const existingDate = acc.dates[admin.request];
+ const adminDate = new Date(admin.occurrence_period_start);
+
+ if (!existingDate || adminDate > new Date(existingDate)) {
+ acc.dates[admin.request] = admin.occurrence_period_start;
+ acc.performers[admin.request] = admin.created_by
+ ? formatName(admin.created_by)
+ : "";
+ }
+
+ return acc;
+ },
+ { dates: {}, performers: {} },
+ );
+ }, [administrations?.results]);
+
+ // Calculate earliest authored date from all medications
+ const getEarliestAuthoredDate = (medications: MedicationRequestRead[]) => {
+ if (!medications?.length) return null;
+ return new Date(
+ Math.min(
+ ...medications.map((med) =>
+ new Date(med.authored_on || med.created_date).getTime(),
+ ),
+ ),
+ );
+ };
+
+ // Calculate if we can go back further based on the earliest slot and authored date
+ const canGoBack = React.useMemo(() => {
+ const medications = showStopped
+ ? [
+ ...(activeMedications?.results || []),
+ ...(stoppedMedications?.results || []),
+ ]
+ : activeMedications?.results || [];
+
+ const earliestAuthoredDate = getEarliestAuthoredDate(medications);
+ if (!earliestAuthoredDate || !visibleSlots.length) return true;
+
+ const firstSlotDate = new Date(visibleSlots[0].date);
+ const [startHour] = visibleSlots[0].start.split(":").map(Number);
+ firstSlotDate.setHours(startHour, 0, 0, 0);
+
+ return firstSlotDate > earliestAuthoredDate;
+ }, [activeMedications, stoppedMedications, showStopped, visibleSlots]);
+
+ // State for administration
+ const [selectedMedication, setSelectedMedication] =
+ useState(null);
+ const [dialogOpen, setDialogOpen] = useState(false);
+ const [isSheetOpen, setIsSheetOpen] = useState(false);
+ const [administrationRequest, setAdministrationRequest] =
+ useState(null);
+
+ // Calculate last modified date
+ const lastModifiedDate = React.useMemo(() => {
+ if (!administrations?.results?.length) return null;
+
+ const sortedAdmins = [...administrations.results].sort(
+ (a, b) =>
+ new Date(b.occurrence_period_start).getTime() -
+ new Date(a.occurrence_period_start).getTime(),
+ );
+
+ return new Date(sortedAdmins[0].occurrence_period_start);
+ }, [administrations?.results]);
+
+ // Mutations
+ const { mutate: discontinueMedication } = useMutation({
+ mutationFn: mutate(medicationRequestApi.upsert, {
+ pathParams: { patientId },
+ }),
+ onSuccess: () => {
+ refetchActive();
+ refetchStopped();
+ },
+ });
+
+ // Handlers
+ const handlePreviousSlot = React.useCallback(() => {
+ if (!canGoBack) return;
+
+ const newEndSlotIndex = endSlotIndex - 1;
+ if (newEndSlotIndex < 0) {
+ setEndSlotIndex(3);
+ const newDate = new Date(endSlotDate);
+ newDate.setDate(newDate.getDate() - 1);
+ setEndSlotDate(newDate);
+ } else {
+ setEndSlotIndex(newEndSlotIndex);
+ }
+ }, [endSlotDate, endSlotIndex, canGoBack]);
+
+ const handleNextSlot = React.useCallback(() => {
+ const newEndSlotIndex = endSlotIndex + 1;
+ if (newEndSlotIndex > 3) {
+ setEndSlotIndex(0);
+ const newDate = new Date(endSlotDate);
+ newDate.setDate(newDate.getDate() + 1);
+ setEndSlotDate(newDate);
+ } else {
+ setEndSlotIndex(newEndSlotIndex);
+ }
+ }, [endSlotDate, endSlotIndex]);
+
+ const handleAdminister = React.useCallback(
+ (medication: MedicationRequestRead) => {
+ setAdministrationRequest(
+ createMedicationAdministrationRequest(medication, encounterId),
+ );
+ setSelectedMedication(medication);
+ setDialogOpen(true);
+ },
+ [encounterId],
+ );
+
+ const handleEditAdministration = React.useCallback(
+ (medication: MedicationRequestRead, admin: MedicationAdministration) => {
+ setAdministrationRequest({
+ id: admin.id,
+ request: admin.request,
+ encounter: admin.encounter,
+ note: admin.note || "",
+ occurrence_period_start: admin.occurrence_period_start,
+ occurrence_period_end: admin.occurrence_period_end,
+ status: admin.status,
+ medication: admin.medication,
+ dosage: admin.dosage,
+ });
+ setSelectedMedication(medication);
+ setDialogOpen(true);
+ },
+ [],
+ );
+
+ const handleDiscontinue = React.useCallback(
+ (medication: MedicationRequestRead) => {
+ discontinueMedication({
+ datapoints: [
+ {
+ ...medication,
+ status: "ended",
+ encounter: encounterId,
+ },
+ ],
+ });
+ },
+ [discontinueMedication, encounterId],
+ );
+
+ const medications = showStopped
+ ? [
+ ...(activeMedications?.results || []),
+ ...(stoppedMedications?.results || []),
+ ]
+ : activeMedications?.results || [];
+
+ if (!activeMedications || !stoppedMedications) {
+ return (
+
+
+
+ );
+ }
+
+ if (!medications?.length) {
+ return (
+
+ );
+ }
+
+ return (
+
+
+ setIsSheetOpen(true)}
+ >
+
+ {t("administer_medicine")}
+
+
+
+
+
+
+ {/* Top row without vertical borders */}
+
+
+
+ {lastModifiedDate && (
+
+ {t("last_modified")}{" "}
+ {formatDistanceToNow(lastModifiedDate)} {t("ago")}
+
+ )}
+
+
+
+
+
+
+
+ {visibleSlots.map((slot) => (
+
+ ))}
+
+
+
+
+
+
+
+ {/* Main content with borders */}
+
+ {/* Headers */}
+
+ {t("medicine")}:
+
+ {visibleSlots.map((slot, i) => (
+
+ {i === endSlotIndex &&
+ slot.date.getTime() === currentDate.getTime() && (
+
+ )}
+ {slot.label}
+
+ ))}
+
+
+ {/* Medication rows */}
+ {medications?.map((medication) => (
+
+ ))}
+
+
+
+ {stoppedMedications?.results?.length > 0 && (
+ setShowStopped(!showStopped)}
+ >
+
+
+ {showStopped ? t("hide") : t("show")}{" "}
+ {`${stoppedMedications?.results?.length} ${t("stopped")}`}{" "}
+ {t("prescriptions")}
+
+
+ )}
+
+
+
+
+ {selectedMedication && administrationRequest && (
+
{
+ setDialogOpen(open);
+ if (!open) {
+ setAdministrationRequest(null);
+ setSelectedMedication(null);
+ refetchAdministrations();
+ }
+ }}
+ medication={selectedMedication}
+ lastAdministeredDate={
+ lastAdministeredDetails?.dates[selectedMedication.id]
+ }
+ lastAdministeredBy={
+ lastAdministeredDetails?.performers[selectedMedication.id]
+ }
+ administrationRequest={administrationRequest}
+ patientId={patientId}
+ />
+ )}
+
+ {
+ setIsSheetOpen(open);
+ if (!open) {
+ refetchAdministrations();
+ }
+ }}
+ medications={medications}
+ lastAdministeredDates={lastAdministeredDetails?.dates}
+ patientId={patientId}
+ encounterId={encounterId}
+ />
+
+ );
+};
diff --git a/src/components/Medicine/MedicationAdministration/MedicineAdminDialog.tsx b/src/components/Medicine/MedicationAdministration/MedicineAdminDialog.tsx
new file mode 100644
index 00000000000..3668e4f84de
--- /dev/null
+++ b/src/components/Medicine/MedicationAdministration/MedicineAdminDialog.tsx
@@ -0,0 +1,111 @@
+"use client";
+
+import { useMutation } from "@tanstack/react-query";
+import { t } from "i18next";
+import React from "react";
+import { toast } from "sonner";
+
+import { Button } from "@/components/ui/button";
+import {
+ Dialog,
+ DialogContent,
+ DialogFooter,
+ DialogHeader,
+ DialogTitle,
+} from "@/components/ui/dialog";
+
+import mutate from "@/Utils/request/mutate";
+import { MedicationAdministrationRequest } from "@/types/emr/medicationAdministration/medicationAdministration";
+import medicationAdministrationApi from "@/types/emr/medicationAdministration/medicationAdministrationApi";
+import { MedicationRequestRead } from "@/types/emr/medicationRequest";
+
+import { MedicineAdminForm } from "./MedicineAdminForm";
+
+interface Props {
+ open: boolean;
+ onOpenChange: (open: boolean) => void;
+ medication: MedicationRequestRead;
+ lastAdministeredDate?: string;
+ lastAdministeredBy?: string;
+ administrationRequest: MedicationAdministrationRequest;
+ patientId: string;
+}
+
+export const MedicineAdminDialog = ({
+ open,
+ onOpenChange,
+ medication,
+ lastAdministeredDate,
+ lastAdministeredBy,
+ administrationRequest: initialRequest,
+ patientId,
+}: Props) => {
+ const [administrationRequest, setAdministrationRequest] =
+ React.useState(initialRequest);
+ const [isFormValid, setIsFormValid] = React.useState(true);
+
+ // Update state when initialRequest changes
+ React.useEffect(() => {
+ setAdministrationRequest(initialRequest);
+ }, [initialRequest]);
+
+ const { mutate: upsertAdministration, isPending } = useMutation({
+ mutationFn: mutate(
+ medicationAdministrationApi.upsertMedicationAdministration,
+ {
+ pathParams: { patientId: patientId },
+ },
+ ),
+ onSuccess: () => {
+ onOpenChange(false);
+ toast.success(t("medication_administration_saved"));
+ },
+ });
+
+ const handleSubmit = () => {
+ upsertAdministration({
+ datapoints: [administrationRequest],
+ });
+ };
+
+ return (
+
+
+
+
+
+ {administrationRequest.id
+ ? t("edit_administration")
+ : t("administer_medicine")}
+
+
+
+
+
+
+
+
+
+ onOpenChange(false)}>
+ {t("cancel")}
+
+
+ {isPending
+ ? t("saving")
+ : administrationRequest.id
+ ? t("update")
+ : t("administer_medicine")}
+
+
+
+
+ );
+};
diff --git a/src/components/Medicine/MedicationAdministration/MedicineAdminForm.tsx b/src/components/Medicine/MedicationAdministration/MedicineAdminForm.tsx
new file mode 100644
index 00000000000..62b48d65b65
--- /dev/null
+++ b/src/components/Medicine/MedicationAdministration/MedicineAdminForm.tsx
@@ -0,0 +1,422 @@
+"use client";
+
+import { format, formatDistanceToNow } from "date-fns";
+import { t } from "i18next";
+import React, { useEffect, useState } from "react";
+
+import { cn } from "@/lib/utils";
+
+import CareIcon from "@/CAREUI/icons/CareIcon";
+
+import { Button } from "@/components/ui/button";
+import { Calendar } from "@/components/ui/calendar";
+import { Input } from "@/components/ui/input";
+import { Label } from "@/components/ui/label";
+import {
+ Popover,
+ PopoverContent,
+ PopoverTrigger,
+} from "@/components/ui/popover";
+import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
+import {
+ Select,
+ SelectContent,
+ SelectItem,
+ SelectTrigger,
+ SelectValue,
+} from "@/components/ui/select";
+
+import { getFrequencyDisplay } from "@/components/Medicine/MedicationsTable";
+import { formatDosage } from "@/components/Medicine/utils";
+
+import { formatName } from "@/Utils/utils";
+import {
+ MEDICATION_ADMINISTRATION_STATUS,
+ MedicationAdministrationRequest,
+ MedicationAdministrationStatus,
+} from "@/types/emr/medicationAdministration/medicationAdministration";
+import { MedicationRequestRead } from "@/types/emr/medicationRequest";
+
+interface MedicineAdminFormProps {
+ medication: MedicationRequestRead;
+ lastAdministeredDate?: string;
+ lastAdministeredBy?: string;
+ administrationRequest: MedicationAdministrationRequest;
+ onChange: (request: MedicationAdministrationRequest) => void;
+ formId: string;
+ isValid?: (valid: boolean) => void;
+}
+
+export const MedicineAdminForm: React.FC = ({
+ medication,
+ lastAdministeredDate,
+ lastAdministeredBy,
+ administrationRequest,
+ onChange,
+ formId,
+ isValid,
+}) => {
+ const [isPastTime, setIsPastTime] = useState(
+ administrationRequest.occurrence_period_start !==
+ administrationRequest.occurrence_period_end || !!administrationRequest.id,
+ );
+ const [startTimeError, setStartTimeError] = useState("");
+ const [endTimeError, setEndTimeError] = useState("");
+
+ const validateDateTime = (date: Date, isStartTime: boolean): string => {
+ const now = new Date();
+ const authoredOn = new Date(medication.authored_on);
+ const startTime = new Date(administrationRequest.occurrence_period_start);
+
+ if (date > now) {
+ return t(
+ isStartTime ? "start_time_future_error" : "end_time_future_error",
+ );
+ }
+
+ if (isStartTime) {
+ return date < authoredOn ? t("start_time_before_authored_error") : "";
+ }
+
+ return date < startTime ? t("end_time_before_start_error") : "";
+ };
+
+ // Validate and notify parent whenever times change
+ useEffect(() => {
+ if (
+ !administrationRequest.occurrence_period_start ||
+ !administrationRequest.occurrence_period_end
+ ) {
+ isValid?.(false);
+ return;
+ }
+
+ const startDate = new Date(administrationRequest.occurrence_period_start);
+ const endDate = new Date(administrationRequest.occurrence_period_end);
+
+ const startError = validateDateTime(startDate, true);
+ const endError = validateDateTime(endDate, false);
+
+ setStartTimeError(startError);
+ setEndTimeError(endError);
+
+ isValid?.(!startError && !endError);
+ }, [
+ administrationRequest.occurrence_period_start,
+ administrationRequest.occurrence_period_end,
+ isValid,
+ validateDateTime,
+ ]);
+
+ const handleDateChange = (newTime: string, isStartTime: boolean) => {
+ const date = new Date(newTime);
+
+ // Preserve existing time if available
+ const existingDateStr = isStartTime
+ ? administrationRequest.occurrence_period_start
+ : administrationRequest.occurrence_period_end;
+
+ if (existingDateStr) {
+ const existingDate = new Date(existingDateStr);
+ date.setHours(existingDate.getHours());
+ date.setMinutes(existingDate.getMinutes());
+ }
+
+ onChange({
+ ...administrationRequest,
+ ...(isStartTime
+ ? {
+ occurrence_period_start: date.toISOString(),
+ occurrence_period_end: date.toISOString(),
+ }
+ : {
+ occurrence_period_end: date.toISOString(),
+ }),
+ });
+ };
+
+ const formatTime = (date: string | undefined) => {
+ if (!date) return "";
+ try {
+ const dateObj = new Date(date);
+ if (isNaN(dateObj.getTime())) return "";
+ return `${dateObj.getHours().toString().padStart(2, "0")}:${dateObj
+ .getMinutes()
+ .toString()
+ .padStart(2, "0")}`;
+ } catch {
+ return "";
+ }
+ };
+
+ const handleTimeChange = (
+ event: React.ChangeEvent,
+ isStartTime: boolean,
+ ) => {
+ const [hours, minutes] = event.target.value.split(":").map(Number);
+ if (isNaN(hours) || isNaN(minutes)) return;
+
+ const dateStr = isStartTime
+ ? administrationRequest.occurrence_period_start
+ : administrationRequest.occurrence_period_end;
+
+ if (!dateStr) return;
+
+ const currentDate = new Date(dateStr);
+ if (isNaN(currentDate.getTime())) return;
+
+ currentDate.setHours(hours);
+ currentDate.setMinutes(minutes);
+
+ onChange({
+ ...administrationRequest,
+ ...(isStartTime
+ ? {
+ occurrence_period_start: currentDate.toISOString(),
+ occurrence_period_end: currentDate.toISOString(),
+ }
+ : {
+ occurrence_period_end: currentDate.toISOString(),
+ }),
+ });
+ };
+
+ return (
+
+
+
+ {medication.medication?.display}
+
+ {lastAdministeredDate && (
+
+ {t("last_administered")}{" "}
+ {formatDistanceToNow(new Date(lastAdministeredDate))} {t("ago")}{" "}
+ {t("by")} {formatName(medication.created_by)}
+
+ )}
+
+ {t("prescribed")}{" "}
+ {formatDistanceToNow(
+ new Date(medication.authored_on || medication.created_date),
+ )}{" "}
+ {t("ago")} {t("by")} {lastAdministeredBy}
+
+
+
+
+
+
{t("dosage")}
+
+ {formatDosage(medication.dosage_instruction[0])}
+
+
+
+
{t("frequency")}
+
+ {getFrequencyDisplay(medication.dosage_instruction[0]?.timing)
+ ?.meaning || "-"}
+
+
+
+
{t("route")}
+
+ {medication.dosage_instruction[0]?.route?.display || "Oral"}
+
+
+
+
{t("duration")}
+
+ {medication.dosage_instruction[0]?.timing?.repeat?.bounds_duration
+ ?.value || "-"}{" "}
+ {medication.dosage_instruction[0]?.timing?.repeat?.bounds_duration
+ ?.unit || ""}
+
+
+
+
+
+ {t("status")}
+
+ onChange({ ...administrationRequest, status: value })
+ }
+ >
+
+
+
+
+ {MEDICATION_ADMINISTRATION_STATUS.map((status) => (
+
+ {t(status)}
+
+ ))}
+
+
+
+
+
+ {t("administration_notes")}
+
+ onChange({ ...administrationRequest, note: e.target.value })
+ }
+ />
+
+
+ {!administrationRequest.id && (
+
+
{t("is_this_administration_for_a_past_time")}?
+
{
+ setIsPastTime(newValue === "yes");
+ if (newValue === "no") {
+ // Set both times to current time
+ const now = new Date().toISOString();
+ onChange({
+ ...administrationRequest,
+ occurrence_period_start: now,
+ occurrence_period_end: now,
+ });
+ }
+ }}
+ className="flex gap-4"
+ >
+
+
+ {t("yes")}
+
+
+
+ {t("no")}
+
+
+
+ )}
+
+
+
{t("start_time")}
+
+
+
+
+
+ {administrationRequest.occurrence_period_start
+ ? format(
+ new Date(administrationRequest.occurrence_period_start),
+ "PPP",
+ )
+ : t("pick_a_date")}
+
+
+
+ {
+ if (!date) return;
+ handleDateChange(date.toISOString(), true);
+ }}
+ initialFocus
+ disabled={(date) => {
+ const now = new Date();
+ const encounterStart = new Date(medication.authored_on);
+ return date < encounterStart || date > now;
+ }}
+ />
+
+
+
handleTimeChange(e, true)}
+ disabled={!isPastTime || !!administrationRequest.id}
+ />
+
+ {startTimeError && (
+
{startTimeError}
+ )}
+
+
+
+
{t("end_time")}
+
+
+
+
+
+ {administrationRequest.occurrence_period_end
+ ? format(
+ new Date(administrationRequest.occurrence_period_end),
+ "PPP",
+ )
+ : t("pick_a_date")}
+
+
+
+ {
+ if (!date) return;
+ handleDateChange(date.toISOString(), false);
+ }}
+ initialFocus
+ disabled={(date) => {
+ const now = new Date();
+ const encounterStart = new Date(medication.authored_on);
+ return date < encounterStart || date > now;
+ }}
+ />
+
+
+
handleTimeChange(e, false)}
+ disabled={
+ !isPastTime ||
+ (!!administrationRequest.id &&
+ administrationRequest.status !== "in_progress")
+ }
+ />
+
+ {endTimeError &&
{endTimeError}
}
+
+
+ );
+};
diff --git a/src/components/Medicine/MedicationAdministration/MedicineAdminSheet.tsx b/src/components/Medicine/MedicationAdministration/MedicineAdminSheet.tsx
new file mode 100644
index 00000000000..80b212081c4
--- /dev/null
+++ b/src/components/Medicine/MedicationAdministration/MedicineAdminSheet.tsx
@@ -0,0 +1,287 @@
+"use client";
+
+import { useMutation } from "@tanstack/react-query";
+import { t } from "i18next";
+import { Search } from "lucide-react";
+import React, { useCallback, useMemo, useRef, useState } from "react";
+import { toast } from "sonner";
+
+import { Button } from "@/components/ui/button";
+import { Checkbox } from "@/components/ui/checkbox";
+import { Input } from "@/components/ui/input";
+import {
+ Sheet,
+ SheetContent,
+ SheetFooter,
+ SheetHeader,
+ SheetTitle,
+} from "@/components/ui/sheet";
+
+import mutate from "@/Utils/request/mutate";
+import { MedicationAdministrationRequest } from "@/types/emr/medicationAdministration/medicationAdministration";
+import medicationAdministrationApi from "@/types/emr/medicationAdministration/medicationAdministrationApi";
+import { MedicationRequestRead } from "@/types/emr/medicationRequest";
+
+import { MedicineAdminForm } from "./MedicineAdminForm";
+import { createMedicationAdministrationRequest } from "./utils";
+
+interface Props {
+ open: boolean;
+ onOpenChange: (open: boolean) => void;
+ medications: MedicationRequestRead[];
+ lastAdministeredDates?: Record;
+ patientId: string;
+ encounterId: string;
+}
+
+interface MedicineListItemProps {
+ medicine: MedicationRequestRead;
+ isSelected: boolean;
+ onSelect: (checked: boolean) => void;
+ administrationRequest?: MedicationAdministrationRequest;
+ lastAdministeredDate?: string;
+ lastAdministeredBy?: string;
+ onAdministrationChange: (request: MedicationAdministrationRequest) => void;
+ isValid: (valid: boolean) => void;
+}
+
+const MedicineListItem = ({
+ medicine,
+ isSelected,
+ onSelect,
+ administrationRequest,
+ lastAdministeredDate,
+ lastAdministeredBy,
+ onAdministrationChange,
+ isValid,
+}: MedicineListItemProps) => {
+ const medicationDisplay = medicine.medication?.display;
+
+ return (
+
+
+
+
+ {medicationDisplay}
+ {medicine.dosage_instruction[0]?.as_needed_boolean && (
+
+ {t("as_needed_prn")}
+
+ )}
+
+
+
+
+
+
+
+ {isSelected && administrationRequest && (
+
+ )}
+
+
+
+ );
+};
+
+export function MedicineAdminSheet({
+ open,
+ onOpenChange,
+ medications,
+ lastAdministeredDates,
+ patientId,
+ encounterId,
+}: Props) {
+ const [selectedMedicines, setSelectedMedicines] = useState>(
+ new Set(),
+ );
+ const [administrationRequests, setAdministrationRequests] = useState<
+ Record
+ >({});
+ const [formValidation, setFormValidation] = useState>(
+ {},
+ );
+ const [search, setSearch] = useState("");
+ const formRef = useRef(null);
+
+ const { mutate: upsertAdministrations, isPending } = useMutation({
+ mutationFn: mutate(
+ medicationAdministrationApi.upsertMedicationAdministration,
+ {
+ pathParams: { patientId },
+ },
+ ),
+ onSuccess: () => {
+ toast.success(t("medication_administration_saved"));
+ handleClose();
+ },
+ });
+
+ const filteredMedicines = medications.filter((medicine) => {
+ const display = medicine.medication?.display;
+ return (
+ typeof display === "string" &&
+ display.toLowerCase().includes(search.toLowerCase())
+ );
+ });
+
+ const handleSelect = useCallback(
+ (id: string, checked: boolean) => {
+ setSelectedMedicines((prev) => {
+ const next = new Set(prev);
+ if (checked) {
+ next.add(id);
+ const medicine = medications.find((m) => m.id === id);
+ if (medicine?.medication?.display) {
+ setAdministrationRequests((prev) => ({
+ ...prev,
+ [id]: createMedicationAdministrationRequest(
+ medicine,
+ encounterId,
+ ),
+ }));
+ }
+ } else {
+ next.delete(id);
+ setAdministrationRequests((prev) => {
+ const { [id]: _, ...rest } = prev;
+ return rest;
+ });
+ }
+ return next;
+ });
+ },
+ [medications, encounterId],
+ );
+
+ const handleSubmit = (e: React.FormEvent) => {
+ e.preventDefault();
+ const administrations = Array.from(selectedMedicines).map(
+ (id) => administrationRequests[id],
+ );
+ upsertAdministrations({
+ datapoints: administrations,
+ });
+ };
+
+ const handleClose = () => {
+ onOpenChange(false);
+ setSelectedMedicines(new Set());
+ setAdministrationRequests({});
+ };
+
+ const handleAdministrationChange = useCallback(
+ (medicineId: string, request: MedicationAdministrationRequest) => {
+ setAdministrationRequests((prev) => ({
+ ...prev,
+ [medicineId]: request,
+ }));
+ },
+ [],
+ );
+
+ const handleFormValidation = useCallback(
+ (medicineId: string, isValid: boolean) => {
+ setFormValidation((prev) => ({
+ ...prev,
+ [medicineId]: isValid,
+ }));
+ },
+ [],
+ );
+
+ const isAllFormsValid = useMemo(
+ () =>
+ Array.from(selectedMedicines).every((id) => formValidation[id] !== false),
+ [selectedMedicines, formValidation],
+ );
+
+ return (
+
+
+
+
+
+ );
+}
diff --git a/src/components/Medicine/MedicationAdministration/utils.ts b/src/components/Medicine/MedicationAdministration/utils.ts
new file mode 100644
index 00000000000..377f2febb1d
--- /dev/null
+++ b/src/components/Medicine/MedicationAdministration/utils.ts
@@ -0,0 +1,116 @@
+import { format } from "date-fns";
+
+import { MedicationAdministrationRequest } from "@/types/emr/medicationAdministration/medicationAdministration";
+import { MedicationRequestRead } from "@/types/emr/medicationRequest";
+
+// Constants
+export const TIME_SLOTS = [
+ { label: "12:00 AM - 06:00 AM", start: "00:00", end: "06:00" },
+ { label: "06:00 AM - 12:00 PM", start: "06:00", end: "12:00" },
+ { label: "12:00 PM - 06:00 PM", start: "12:00", end: "18:00" },
+ { label: "06:00 PM - 12:00 AM", start: "18:00", end: "24:00" },
+] as const;
+
+export const STATUS_COLORS = {
+ completed: "bg-emerald-50 text-emerald-700 border-emerald-200",
+ in_progress: "bg-yellow-50 text-yellow-700 border-yellow-200",
+ default: "bg-red-50 text-red-700 border-red-200",
+} as const;
+
+// Utility Functions
+export function createMedicationAdministrationRequest(
+ medication: MedicationRequestRead,
+ encounterId: string,
+): MedicationAdministrationRequest {
+ return {
+ request: medication.id,
+ encounter: encounterId,
+ medication: {
+ code: medication.medication?.code,
+ display: medication.medication?.display,
+ system: medication.medication?.system,
+ },
+ occurrence_period_start: format(new Date(), "yyyy-MM-dd'T'HH:mm"),
+ occurrence_period_end: format(new Date(), "yyyy-MM-dd'T'HH:mm"),
+ note: "",
+ status: "completed",
+ dosage: {
+ site: medication.dosage_instruction[0]?.site,
+ route: medication.dosage_instruction[0]?.route,
+ method: medication.dosage_instruction[0]?.method,
+ dose: medication.dosage_instruction[0]?.dose_and_rate?.dose_quantity && {
+ value:
+ medication.dosage_instruction[0]?.dose_and_rate?.dose_quantity?.value,
+ unit: medication.dosage_instruction[0]?.dose_and_rate?.dose_quantity
+ ?.unit,
+ },
+ },
+ };
+}
+
+export function isTimeInSlot(
+ date: Date,
+ slot: { date: Date; start: string; end: string },
+): boolean {
+ const slotStartDate = new Date(slot.date);
+ const [startHour] = slot.start.split(":").map(Number);
+ const [endHour] = slot.end.split(":").map(Number);
+
+ slotStartDate.setHours(startHour, 0, 0, 0);
+ const slotEndDate = new Date(slotStartDate);
+ slotEndDate.setHours(endHour, 0, 0, 0);
+
+ return date >= slotStartDate && date < slotEndDate;
+}
+
+export function getAdministrationsForTimeSlot<
+ T extends {
+ occurrence_period_start: string;
+ request: string;
+ },
+>(
+ administrations: T[],
+ medicationId: string,
+ slotDate: Date,
+ start: string,
+ end: string,
+): T[] {
+ return administrations.filter((admin) => {
+ const adminDate = new Date(admin.occurrence_period_start);
+ const slotStartDate = new Date(slotDate);
+ const slotEndDate = new Date(slotDate);
+
+ const [startHour] = start.split(":").map(Number);
+ const [endHour] = end.split(":").map(Number);
+
+ slotStartDate.setHours(startHour, 0, 0, 0);
+ slotEndDate.setHours(endHour, 0, 0, 0);
+
+ return (
+ admin.request === medicationId &&
+ adminDate >= slotStartDate &&
+ adminDate < slotEndDate
+ );
+ });
+}
+
+export function getCurrentTimeSlotIndex(): number {
+ const hour = new Date().getHours();
+ if (hour < 6) return 0;
+ if (hour < 12) return 1;
+ if (hour < 18) return 2;
+ return 3;
+}
+
+export function getEarliestAuthoredDate(
+ medications: MedicationRequestRead[],
+): Date | null {
+ if (!medications?.length) return null;
+ return new Date(
+ Math.min(
+ ...medications.map((med) =>
+ new Date(med.authored_on || med.created_date).getTime(),
+ ),
+ ),
+ );
+}
diff --git a/src/components/Medicine/MedicationRequestTable/index.tsx b/src/components/Medicine/MedicationRequestTable/index.tsx
index 92210bf98b9..389093ae429 100644
--- a/src/components/Medicine/MedicationRequestTable/index.tsx
+++ b/src/components/Medicine/MedicationRequestTable/index.tsx
@@ -1,23 +1,54 @@
import { useQuery } from "@tanstack/react-query";
+import { t } from "i18next";
import { PencilIcon } from "lucide-react";
import { Link } from "raviger";
import { useState } from "react";
-import SubHeading from "@/CAREUI/display/SubHeading";
import CareIcon from "@/CAREUI/icons/CareIcon";
-import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { ScrollArea, ScrollBar } from "@/components/ui/scroll-area";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import Loading from "@/components/Common/Loading";
+import { AdministrationTab } from "@/components/Medicine/MedicationAdministration/AdministrationTab";
import { MedicationsTable } from "@/components/Medicine/MedicationsTable";
import query from "@/Utils/request/query";
import { MedicationRequestRead } from "@/types/emr/medicationRequest";
import medicationRequestApi from "@/types/emr/medicationRequest/medicationRequestApi";
+interface EmptyStateProps {
+ searching?: boolean;
+ searchQuery?: string;
+ message?: string;
+ description?: string;
+}
+
+export const EmptyState = ({
+ searching,
+ searchQuery,
+ message,
+ description,
+}: EmptyStateProps) => (
+
+
+
+
+
+
+ {message || (searching ? "No matches found" : "No Prescriptions")}
+
+
+ {description ||
+ (searching
+ ? `No medications match "${searchQuery}"`
+ : "No medications have been prescribed yet")}
+
+
+
+);
+
interface Props {
readonly?: boolean;
facilityId: string;
@@ -26,22 +57,49 @@ interface Props {
}
export default function MedicationRequestTable({
- facilityId,
patientId,
encounterId,
+ facilityId,
}: Props) {
const [searchQuery, setSearchQuery] = useState("");
+ const [showStopped, setShowStopped] = useState(false);
+
+ const { data: activeMedications, isLoading: loadingActive } = useQuery({
+ queryKey: ["medication_requests_active", patientId],
+ queryFn: query(medicationRequestApi.list, {
+ pathParams: { patientId: patientId },
+ queryParams: {
+ encounter: encounterId,
+ limit: 100,
+ status: ["active", "on-hold", "draft", "unknown"].join(","),
+ },
+ }),
+ enabled: !!patientId,
+ });
- const { data: medications, isLoading: loading } = useQuery({
- queryKey: ["medication_requests", patientId],
+ const { data: stoppedMedications, isLoading: loadingStopped } = useQuery({
+ queryKey: ["medication_requests_stopped", patientId],
queryFn: query(medicationRequestApi.list, {
pathParams: { patientId: patientId },
- queryParams: { encounter: encounterId },
+ queryParams: {
+ encounter: encounterId,
+ limit: 100,
+ status: ["ended", "completed", "cancelled", "entered_in_error"].join(
+ ",",
+ ),
+ },
}),
enabled: !!patientId,
});
- const filteredMedications = medications?.results?.filter(
+ const medications = showStopped
+ ? [
+ ...(activeMedications?.results || []),
+ ...(stoppedMedications?.results || []),
+ ]
+ : activeMedications?.results || [];
+
+ const displayedMedications = medications.filter(
(med: MedicationRequestRead) => {
if (!searchQuery.trim()) return true;
const searchTerm = searchQuery.toLowerCase().trim();
@@ -50,123 +108,114 @@ export default function MedicationRequestTable({
},
);
- const activeMedications = filteredMedications?.filter(
- (med: MedicationRequestRead) =>
- ["active", "on_hold"].includes(med.status || ""),
- );
- const discontinuedMedications = filteredMedications?.filter(
- (med: MedicationRequestRead) =>
- !["active", "on_hold"].includes(med.status || ""),
- );
-
- const EmptyState = ({ searching }: { searching?: boolean }) => (
-
-
-
-
-
-
- {searching ? "No matches found" : "No Prescriptions"}
-
-
- {searching
- ? `No medications match "${searchQuery}"`
- : "No medications have been prescribed yet"}
-
-
-
- );
+ const isLoading = loadingActive || loadingStopped;
return (
-
-
-
-
- Edit
-
-
-
-
-
- Print
-
-
-
- }
- />
-
-
-
-
-
setSearchQuery(e.target.value)}
- className="flex-1 bg-transparent text-sm outline-none placeholder:text-gray-500"
- />
- {searchQuery && (
-
setSearchQuery("")}
+
+
+
+
-
-
- )}
-
-
- {loading ? (
-
-
-
- ) : !medications?.results?.length ? (
-
- ) : !filteredMedications?.length ? (
-
- ) : (
-
-
-
-
-
- Active{" "}
-
- {activeMedications?.length || 0}
-
-
-
- Discontinued{" "}
-
- {discontinuedMedications?.length || 0}
-
-
-
-
+ {t("prescriptions")}
+
+
+ {t("medicine_administration")}
+
+
-
-
-
-
-
-
+
+
+
+
+ setSearchQuery(e.target.value)}
+ className="flex-1 bg-transparent text-sm outline-none placeholder:text-gray-500"
/>
-
+ {searchQuery && (
+ setSearchQuery("")}
+ >
+
+
+ )}
+
+
+
+
+
+ {t("edit")}
+
+
+
+
+
+ {t("print")}
+
+
+
-
-
-
- )}
+
+ {isLoading ? (
+
+
+
+ ) : !medications.length ? (
+
+ ) : !displayedMedications.length ? (
+
+ ) : (
+
+
+
+
+
+ {!!stoppedMedications?.results?.length && (
+
setShowStopped(!showStopped)}
+ >
+
+
+ {showStopped ? t("hide") : t("show")}{" "}
+ {`${stoppedMedications?.results?.length} ${t("stopped")}`}{" "}
+ {t("prescriptions")}
+
+
+ )}
+
+
+
+ )}
+
+
+
+
+
+
+
);
diff --git a/src/components/Medicine/MedicationsTable.tsx b/src/components/Medicine/MedicationsTable.tsx
index 059e35bd4d4..05a8e82bada 100644
--- a/src/components/Medicine/MedicationsTable.tsx
+++ b/src/components/Medicine/MedicationsTable.tsx
@@ -19,7 +19,7 @@ import {
import { formatDosage, formatSig } from "./utils";
-function getFrequencyDisplay(
+export function getFrequencyDisplay(
timing?: MedicationRequestDosageInstruction["timing"],
) {
if (!timing) return undefined;
diff --git a/src/components/Medicine/MultiValueSetSelect.tsx b/src/components/Medicine/MultiValueSetSelect.tsx
index f80415b8254..c3da4742235 100644
--- a/src/components/Medicine/MultiValueSetSelect.tsx
+++ b/src/components/Medicine/MultiValueSetSelect.tsx
@@ -55,6 +55,7 @@ export function MultiValueSetSelect({
- {currentValue ? format(currentValue, "PPP") : "Pick a date"}
+ {currentValue ? format(currentValue, "PPP") : t("pick_a_date")}
diff --git a/src/components/Questionnaire/QuestionnaireEditor.tsx b/src/components/Questionnaire/QuestionnaireEditor.tsx
index 880d42b4547..6182d537a83 100644
--- a/src/components/Questionnaire/QuestionnaireEditor.tsx
+++ b/src/components/Questionnaire/QuestionnaireEditor.tsx
@@ -427,7 +427,7 @@ export default function QuestionnaireEditor({ id }: QuestionnaireEditorProps) {
))
) : (
-
+
No organizations selected
)}
@@ -458,7 +458,7 @@ export default function QuestionnaireEditor({ id }: QuestionnaireEditorProps) {
{org.name}
{org.description && (
-
+
- {org.description}
)}
@@ -507,7 +507,7 @@ export default function QuestionnaireEditor({ id }: QuestionnaireEditorProps) {
placeholder="unique-identifier-for-questionnaire"
className="font-mono"
/>
-
+
A unique URL-friendly identifier for this questionnaire
diff --git a/src/components/Resource/ResourceList.tsx b/src/components/Resource/ResourceList.tsx
index 7695828f7e2..1ba35b0effe 100644
--- a/src/components/Resource/ResourceList.tsx
+++ b/src/components/Resource/ResourceList.tsx
@@ -52,7 +52,7 @@ function EmptyState() {
{t("no_resources_found")}
-
+
{t("adjust_resource_filters")}
diff --git a/src/components/ui/button.tsx b/src/components/ui/button.tsx
index b794b6a99fb..68c7071a3b0 100644
--- a/src/components/ui/button.tsx
+++ b/src/components/ui/button.tsx
@@ -42,7 +42,7 @@ const buttonVariants = cva(
},
},
defaultVariants: {
- variant: "default",
+ variant: "primary",
size: "default",
},
},
diff --git a/src/components/ui/date-picker.tsx b/src/components/ui/date-picker.tsx
index 9e95c5473c6..bdd2f4e17ee 100644
--- a/src/components/ui/date-picker.tsx
+++ b/src/components/ui/date-picker.tsx
@@ -1,4 +1,5 @@
import { format } from "date-fns";
+import { t } from "i18next";
import { useState } from "react";
import { cn } from "@/lib/utils";
@@ -46,7 +47,7 @@ export function DatePicker({ date, onChange, disabled }: DatePickerProps) {
{format(date, "PPP")}
>
) : (
- Pick a date
+ {t("pick_a_date")}
)}
diff --git a/src/components/ui/date-range-picker.tsx b/src/components/ui/date-range-picker.tsx
index ffbbf482bce..7c61ec94c2d 100644
--- a/src/components/ui/date-range-picker.tsx
+++ b/src/components/ui/date-range-picker.tsx
@@ -1,4 +1,5 @@
import { format } from "date-fns";
+import { t } from "i18next";
import * as React from "react";
import { DateRange } from "react-day-picker";
@@ -51,7 +52,7 @@ export function DateRangePicker({
format(date.from, "LLL dd, y")
)
) : (
- Pick a date
+ {t("pick_a_date")}
)}
diff --git a/src/pages/Facility/settings/locations/components/LocationCard.tsx b/src/pages/Facility/settings/locations/components/LocationCard.tsx
index 60a427a4945..e12d031d6f2 100644
--- a/src/pages/Facility/settings/locations/components/LocationCard.tsx
+++ b/src/pages/Facility/settings/locations/components/LocationCard.tsx
@@ -82,7 +82,7 @@ export function LocationCard({ location, onEdit, className }: Props) {
{location.name}
-
+
{getLocationFormLabel(location.form)}
diff --git a/src/types/emr/medicationAdministration/medicationAdministration.ts b/src/types/emr/medicationAdministration/medicationAdministration.ts
new file mode 100644
index 00000000000..a736846de4a
--- /dev/null
+++ b/src/types/emr/medicationAdministration/medicationAdministration.ts
@@ -0,0 +1,96 @@
+import { UserBareMinimum } from "@/components/Users/models";
+
+import { DosageQuantity } from "@/types/emr/medicationRequest";
+import { Code } from "@/types/questionnaire/code";
+import { Quantity } from "@/types/questionnaire/quantity";
+
+export const MEDICATION_ADMINISTRATION_STATUS = [
+ "completed",
+ "not_done",
+ "entered_in_error",
+ "stopped",
+ "in_progress",
+ "on_hold",
+ "unknown",
+ "cancelled",
+] as const;
+
+export type MedicationAdministrationStatus =
+ (typeof MEDICATION_ADMINISTRATION_STATUS)[number];
+
+export interface MedicationAdministration {
+ readonly id?: string;
+ status: MedicationAdministrationStatus;
+ status_reason?: Code;
+ category?: "inpatient" | "outpatient" | "community";
+
+ medication: Code;
+
+ authored_on?: string; // datetime
+ occurrence_period_start: string; // datetime
+ occurrence_period_end?: string; // datetime
+ recorded?: string; // datetime
+
+ encounter: string; // uuid
+ request: string; // uuid
+
+ performer?: {
+ actor: string; // uuid
+ function: "performer" | "verifier" | "witness";
+ }[];
+ dosage?: {
+ text?: string;
+ site?: Code;
+ route?: Code;
+ method?: Code;
+ dose?: DosageQuantity;
+ rate?: Quantity;
+ };
+
+ note?: string;
+
+ created_by?: UserBareMinimum;
+ updated_by?: UserBareMinimum;
+}
+
+export interface MedicationAdministrationRequest {
+ id?: string;
+ encounter: string;
+ request: string;
+ status: MedicationAdministrationStatus;
+ status_reason?: Code;
+ medication: Code;
+ occurrence_period_start: string;
+ occurrence_period_end?: string;
+ recorded?: string;
+ note?: string;
+ dosage?: {
+ text?: string;
+ site?: Code;
+ route?: Code;
+ method?: Code;
+ dose?: DosageQuantity;
+ rate?: Quantity;
+ };
+}
+
+export interface MedicationAdministrationRead {
+ id: string;
+ status: MedicationAdministrationStatus;
+ status_reason?: Code;
+ medication: Code;
+ occurrence_period_start: string;
+ occurrence_period_end?: string;
+ recorded?: string;
+ encounter: string;
+ request: string;
+ note?: string;
+ dosage?: {
+ text?: string;
+ site?: Code;
+ route?: Code;
+ method?: Code;
+ dose?: DosageQuantity;
+ rate?: Quantity;
+ };
+}
diff --git a/src/types/emr/medicationAdministration/medicationAdministrationApi.ts b/src/types/emr/medicationAdministration/medicationAdministrationApi.ts
new file mode 100644
index 00000000000..77e37c391e2
--- /dev/null
+++ b/src/types/emr/medicationAdministration/medicationAdministrationApi.ts
@@ -0,0 +1,20 @@
+import { HttpMethod, Type } from "@/Utils/request/api";
+import { PaginatedResponse } from "@/Utils/request/types";
+
+import {
+ MedicationAdministrationRead,
+ MedicationAdministrationRequest,
+} from "./medicationAdministration";
+
+export default {
+ listMedicationAdministrations: {
+ path: "/api/v1/patient/{patientId}/medication/administration/",
+ method: HttpMethod.GET,
+ TRes: Type>(),
+ },
+ upsertMedicationAdministration: {
+ path: "/api/v1/patient/{patientId}/medication/administration/upsert/",
+ method: HttpMethod.POST,
+ TRes: Type,
+ },
+};
diff --git a/src/types/emr/medicationRequest.ts b/src/types/emr/medicationRequest.ts
index 8bd49281e2a..7e84391378a 100644
--- a/src/types/emr/medicationRequest.ts
+++ b/src/types/emr/medicationRequest.ts
@@ -42,16 +42,23 @@ export const UCUM_TIME_UNITS = [
"a",
] as const;
-export const MEDICATION_REQUEST_STATUS = [
+export const ACTIVE_MEDICATION_STATUSES = [
"active",
"on-hold",
+ "draft",
+ "unknown",
+] as const;
+
+export const INACTIVE_MEDICATION_STATUSES = [
"ended",
- "stopped",
"completed",
"cancelled",
"entered_in_error",
- "draft",
- "unknown",
+] as const;
+
+export const MEDICATION_REQUEST_STATUS = [
+ ...ACTIVE_MEDICATION_STATUSES,
+ ...INACTIVE_MEDICATION_STATUSES,
] as const;
export type MedicationRequestStatus =
diff --git a/src/types/emr/medicationRequest/medicationRequestApi.ts b/src/types/emr/medicationRequest/medicationRequestApi.ts
index 5fe8c146b1f..0aed189b863 100644
--- a/src/types/emr/medicationRequest/medicationRequestApi.ts
+++ b/src/types/emr/medicationRequest/medicationRequestApi.ts
@@ -1,14 +1,16 @@
-import { Type } from "@/Utils/request/api";
+import { HttpMethod, Type } from "@/Utils/request/api";
import { PaginatedResponse } from "@/Utils/request/types";
import { MedicationRequestRead } from "@/types/emr/medicationRequest";
-const medicationRequestApi = {
- // Medication
+export default {
list: {
path: "/api/v1/patient/{patientId}/medication/request/",
- method: "GET",
+ method: HttpMethod.GET,
TRes: Type>(),
},
-} as const;
-
-export default medicationRequestApi;
+ upsert: {
+ path: "/api/v1/patient/{patientId}/medication/request/upsert/",
+ method: HttpMethod.POST,
+ TRes: Type,
+ },
+};