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" && ( + + )} +
+ ); + })} + +
+ {ACTIVE_STATUSES.includes( + medication.status as (typeof ACTIVE_STATUSES)[number], + ) && ( + + + + + + + + + )} +
+
+ ); +}; + +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 ( +
+
+ +
+ + + +
+ {/* 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")} + +
+
+ +
+ +
+ + + + + +
+
+ ); +}; 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} +

+
+ +
+
+ +

+ {formatDosage(medication.dosage_instruction[0])} +

+
+
+ +

+ {getFrequencyDisplay(medication.dosage_instruction[0]?.timing) + ?.meaning || "-"} +

+
+
+ +

+ {medication.dosage_instruction[0]?.route?.display || "Oral"} +

+
+
+ +

+ {medication.dosage_instruction[0]?.timing?.repeat?.bounds_duration + ?.value || "-"}{" "} + {medication.dosage_instruction[0]?.timing?.repeat?.bounds_duration + ?.unit || ""} +

+
+
+ +
+ + +
+ +
+ + + onChange({ ...administrationRequest, note: e.target.value }) + } + /> +
+ + {!administrationRequest.id && ( +
+ + { + 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" + > +
+ + +
+
+ + +
+
+
+ )} + +
+ +
+ + + + + + { + 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}

+ )} +
+ +
+ +
+ + + + + + { + 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 ( + + +
+ + + {t("administer_medicines")} + +
+ + setSearch(e.target.value)} + className="pl-8" + /> +
+
+ +
+
+ {filteredMedicines.map((medicine) => ( + handleSelect(medicine.id, checked)} + administrationRequest={administrationRequests[medicine.id]} + lastAdministeredDate={lastAdministeredDates?.[medicine.id]} + onAdministrationChange={(request) => + handleAdministrationChange(medicine.id, request) + } + isValid={(valid) => handleFormValidation(medicine.id, valid)} + /> + ))} +
+
+ + +
+ + +
+
+
+
+
+ ); +} 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 (
- - - -
- } - /> - -
-
- - setSearchQuery(e.target.value)} - className="flex-1 bg-transparent text-sm outline-none placeholder:text-gray-500" - /> - {searchQuery && ( - - )} -
- - {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 && ( + + )} +
+
+ + +
- - - - )} + + {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({ 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, + }, +};