From 891eacda69a6149cd0599881cbb938b0039d4d72 Mon Sep 17 00:00:00 2001 From: Amjith Titus Date: Tue, 14 Jan 2025 19:33:59 +0530 Subject: [PATCH 1/5] feat: add medication request enhancements with combobox quantity input --- public/locale/en.json | 5 + src/common/constants.tsx | 198 ++++++++++ .../Common/ComboboxQuantityInput.tsx | 177 +++++++++ .../MedicationRequestQuestion.tsx | 346 ++++++++++++------ src/pages/Encounters/PrintPrescription.tsx | 310 ++++++---------- 5 files changed, 726 insertions(+), 310 deletions(-) create mode 100644 src/components/Common/ComboboxQuantityInput.tsx diff --git a/public/locale/en.json b/public/locale/en.json index 96f74cf9d81..d926ae7d455 100644 --- a/public/locale/en.json +++ b/public/locale/en.json @@ -915,6 +915,7 @@ "encounter_suggestion_edit_disallowed": "Not allowed to switch to this option in edit consultation", "encounters": "Encounters", "end_datetime": "End Date/Time", + "end_dose": "End Dose", "end_time": "End Time", "enter_aadhaar_number": "Enter a 12-digit Aadhaar ID", "enter_aadhaar_otp": "Enter OTP sent to the registered mobile with Aadhaar", @@ -1856,6 +1857,7 @@ "start_consultation": "Start Consultation", "start_datetime": "Start Date/Time", "start_dosage": "Start Dosage", + "start_dose": "Start Dose", "start_review": "Start Review", "start_time": "Start Time", "start_time_must_be_before_end_time": "Start time must be before end time", @@ -1885,6 +1887,7 @@ "symptoms": "Symptoms", "systolic": "Systolic", "tachycardia": "Tachycardia", + "taper_titrate_dosage": "Taper & Titrate Dosage", "target_dosage": "Target Dosage", "template_deleted": "Template has been deleted", "test_type": "Type of test done", @@ -1951,6 +1954,8 @@ "unit_mo": "Months", "unit_ms": "Milliseconds", "unit_s": "Seconds", + "unit_taper": "Taper", + "unit_titrate": "Titrate", "unit_tsp": "Tsp", "unit_unit(s)": "Unit(s)", "unit_wk": "Weeks", diff --git a/src/common/constants.tsx b/src/common/constants.tsx index a88b50e35b0..7db067922e0 100644 --- a/src/common/constants.tsx +++ b/src/common/constants.tsx @@ -1,5 +1,7 @@ import { IconName } from "@/CAREUI/icons/CareIcon"; +import { MedicationRequest } from "@/types/emr/medicationRequest"; + export const RESULTS_PER_PAGE_LIMIT = 14; /** @@ -909,3 +911,199 @@ export const HEADER_CONTENT_TYPES = { } as const; export const ADMIN_USER_TYPES = ["DistrictAdmin", "StateAdmin"] as const; + +export const MEDICATION_REQUEST_TIMING_OPTIONS = { + BID: { + display: "BID (1-0-1)", + timing: { + repeat: { frequency: 2, period: 1, period_unit: "d" }, + code: { + code: "BID", + display: "Two times a day", + system: "http://terminology.hl7.org/CodeSystem/v3-GTSAbbreviation", + }, + }, + }, + TID: { + display: "TID (1-1-1)", + timing: { + repeat: { frequency: 3, period: 1, period_unit: "d" }, + code: { + code: "TID", + display: "Three times a day", + system: "http://terminology.hl7.org/CodeSystem/v3-GTSAbbreviation", + }, + }, + }, + QID: { + display: "QID (1-1-1-1)", + timing: { + repeat: { frequency: 4, period: 1, period_unit: "d" }, + code: { + code: "QID", + display: "Four times a day", + system: "http://terminology.hl7.org/CodeSystem/v3-GTSAbbreviation", + }, + }, + }, + AM: { + display: "AM (1-0-0)", + timing: { + repeat: { frequency: 1, period: 1, period_unit: "d" }, + code: { + code: "AM", + display: "Every morning", + system: "http://terminology.hl7.org/CodeSystem/v3-GTSAbbreviation", + }, + }, + }, + PM: { + display: "PM (0-0-1)", + timing: { + repeat: { frequency: 1, period: 1, period_unit: "d" }, + code: { + code: "PM", + display: "Every afternoon", + system: "http://terminology.hl7.org/CodeSystem/v3-GTSAbbreviation", + }, + }, + }, + QD: { + display: "QD (Once a day)", + timing: { + repeat: { frequency: 1, period: 1, period_unit: "d" }, + code: { + code: "QD", + display: "Once a day", + system: "http://terminology.hl7.org/CodeSystem/v3-GTSAbbreviation", + }, + }, + }, + QOD: { + display: "QOD (Alternate days)", + timing: { + repeat: { frequency: 1, period: 2, period_unit: "d" }, + code: { + code: "QOD", + display: "Alternate days", + system: "http://terminology.hl7.org/CodeSystem/v3-GTSAbbreviation", + }, + }, + }, + Q1H: { + display: "Q1H (Every 1 hour)", + timing: { + repeat: { frequency: 1, period: 1, period_unit: "h" }, + code: { + code: "Q1H", + display: "Every 1 hour", + system: "http://terminology.hl7.org/CodeSystem/v3-GTSAbbreviation", + }, + }, + }, + Q2H: { + display: "Q2H (Every 2 hours)", + timing: { + repeat: { frequency: 1, period: 2, period_unit: "h" }, + code: { + code: "Q2H", + display: "Every 2 hours", + system: "http://terminology.hl7.org/CodeSystem/v3-GTSAbbreviation", + }, + }, + }, + Q3H: { + display: "Q3H (Every 3 hours)", + timing: { + repeat: { frequency: 1, period: 3, period_unit: "h" }, + code: { + code: "Q3H", + display: "Every 3 hours", + system: "http://terminology.hl7.org/CodeSystem/v3-GTSAbbreviation", + }, + }, + }, + Q4H: { + display: "Q4H (Every 4 hours)", + timing: { + repeat: { frequency: 1, period: 4, period_unit: "h" }, + code: { + code: "Q4H", + display: "Every 4 hours", + system: "http://terminology.hl7.org/CodeSystem/v3-GTSAbbreviation", + }, + }, + }, + Q6H: { + display: "Q6H (Every 6 hours)", + timing: { + repeat: { frequency: 1, period: 6, period_unit: "h" }, + code: { + code: "Q6H", + display: "Every 6 hours", + system: "http://terminology.hl7.org/CodeSystem/v3-GTSAbbreviation", + }, + }, + }, + Q8H: { + display: "Q8H (Every 8 hours)", + timing: { + repeat: { frequency: 1, period: 8, period_unit: "h" }, + code: { + code: "Q8H", + display: "Every 8 hours", + system: "http://terminology.hl7.org/CodeSystem/v3-GTSAbbreviation", + }, + }, + }, + BED: { + display: "BED (0-0-1)", + timing: { + repeat: { frequency: 1, period: 1, period_unit: "d" }, + code: { + code: "BED", + display: "Bedtime", + system: "http://terminology.hl7.org/CodeSystem/v3-GTSAbbreviation", + }, + }, + }, + WK: { + display: "WK (Weekly)", + timing: { + repeat: { frequency: 1, period: 1, period_unit: "wk" }, + code: { + code: "WK", + display: "Weekly", + system: "http://terminology.hl7.org/CodeSystem/v3-GTSAbbreviation", + }, + }, + }, + MO: { + display: "MO (Monthly)", + timing: { + repeat: { frequency: 1, period: 1, period_unit: "mo" }, + code: { + code: "MO", + display: "Monthly", + system: "http://terminology.hl7.org/CodeSystem/v3-GTSAbbreviation", + }, + }, + }, + STAT: { + display: "STAT (Immediately)", + timing: { + repeat: { frequency: 1, period: 1, period_unit: "s" }, + code: { + code: "STAT", + display: "Immediately", + system: "http://terminology.hl7.org/CodeSystem/v3-GTSAbbreviation", + }, + }, + }, +} as const satisfies Record< + string, + { + display: string; + timing: MedicationRequest["dosage_instruction"][0]["timing"]; + } +>; diff --git a/src/components/Common/ComboboxQuantityInput.tsx b/src/components/Common/ComboboxQuantityInput.tsx new file mode 100644 index 00000000000..65f386dad08 --- /dev/null +++ b/src/components/Common/ComboboxQuantityInput.tsx @@ -0,0 +1,177 @@ +"use client"; + +import { t } from "i18next"; +import { Check } from "lucide-react"; +import * as React from "react"; + +import { cn } from "@/lib/utils"; + +import { + Command, + CommandEmpty, + CommandGroup, + CommandItem, + CommandList, +} from "@/components/ui/command"; +import { Input } from "@/components/ui/input"; +import { + Popover, + PopoverContent, + PopoverTrigger, +} from "@/components/ui/popover"; + +interface Props { + quantity?: QuantityValue | null; + onChange: (quantity: QuantityValue) => void; + units: readonly TUnit[]; + disabled?: boolean; + placeholder?: string; + autoFocus?: boolean; +} + +interface QuantityValue { + value?: number; + unit?: TUnit; +} + +export function ComboboxQuantityInput({ + quantity = { value: undefined, unit: undefined }, + onChange, + units, + disabled, + placeholder = "Enter a number...", + autoFocus, +}: Props) { + const [open, setOpen] = React.useState(false); + const [inputValue, setInputValue] = React.useState( + quantity?.value?.toString() || "", + ); + const [selectedUnit, setSelectedUnit] = React.useState( + quantity?.unit, + ); + const inputRef = React.useRef(null); + const [activeIndex, setActiveIndex] = React.useState(-1); + + const showDropdown = /^\d+$/.test(inputValue); + + const handleInputChange = (e: React.ChangeEvent) => { + const value = e.target.value; + if (value === "" || /^\d+$/.test(value)) { + setInputValue(value); + setOpen(true); + setSelectedUnit(undefined); + setActiveIndex(0); + onChange({ + value: value ? parseInt(value, 10) : undefined, + unit: selectedUnit, + }); + } + }; + + const handleKeyDown = (e: React.KeyboardEvent) => { + if (!showDropdown) return; + + if (e.key === "ArrowDown") { + e.preventDefault(); + setOpen(true); + setActiveIndex((prev) => + prev === -1 ? 0 : prev < units.length - 1 ? prev + 1 : prev, + ); + } else if (e.key === "ArrowUp") { + e.preventDefault(); + setActiveIndex((prev) => (prev > 0 ? prev - 1 : prev)); + } else if (e.key === "Enter") { + e.preventDefault(); + if (activeIndex >= 0 && activeIndex < units.length) { + const unit = units[activeIndex]; + setSelectedUnit(unit); + setOpen(false); + setActiveIndex(-1); + onChange({ value: parseInt(inputValue, 10), unit }); + } + } + }; + + React.useEffect(() => { + if (quantity?.value !== undefined) { + setInputValue(quantity.value.toString()); + } + if (quantity?.unit !== undefined) { + setSelectedUnit(quantity.unit); + } + }, [quantity]); + + return ( +
+ + +
+ + {selectedUnit && ( +
+ {t(`unit_${selectedUnit}`)} +
+ )} +
+
+ { + e.preventDefault(); + inputRef.current?.focus(); + }} + > + + + No results found. + + {units.map((unit, index) => ( + { + setSelectedUnit(unit); + setOpen(false); + setActiveIndex(-1); + inputRef.current?.focus(); + onChange({ value: parseInt(inputValue, 10), unit }); + }} + className={cn( + "flex items-center gap-2", + activeIndex === index && "bg-gray-100", + )} + > +
+ {inputValue} {t(`unit_${unit}`)} +
+ +
+ ))} +
+
+
+
+
+
+ ); +} + +export default ComboboxQuantityInput; diff --git a/src/components/Questionnaire/QuestionTypes/MedicationRequestQuestion.tsx b/src/components/Questionnaire/QuestionTypes/MedicationRequestQuestion.tsx index 5fa10d2034f..d46505aed73 100644 --- a/src/components/Questionnaire/QuestionTypes/MedicationRequestQuestion.tsx +++ b/src/components/Questionnaire/QuestionTypes/MedicationRequestQuestion.tsx @@ -20,7 +20,14 @@ import { CollapsibleContent, CollapsibleTrigger, } from "@/components/ui/collapsible"; +import { Dialog, DialogContent } from "@/components/ui/dialog"; +import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; +import { + Popover, + PopoverContent, + PopoverTrigger, +} from "@/components/ui/popover"; import { Select, SelectContent, @@ -29,15 +36,18 @@ import { SelectValue, } from "@/components/ui/select"; -import { QuantityInput } from "@/components/Common/QuantityInput"; +import { ComboboxQuantityInput } from "@/components/Common/ComboboxQuantityInput"; import ValueSetSelect from "@/components/Questionnaire/ValueSetSelect"; import useBreakpoints from "@/hooks/useBreakpoints"; +import { MEDICATION_REQUEST_TIMING_OPTIONS } from "@/common/constants"; + import { BOUNDS_DURATION_UNITS, BoundsDuration, DOSAGE_UNITS, + DoseRange, MEDICATION_REQUEST_INTENT, MedicationRequest, MedicationRequestDosageInstruction, @@ -163,8 +173,8 @@ export function MedicationRequestQuestion({ -
- {medications.length > 0 && ( + {medications.length > 0 && ( +
- )} -
+
+ )}
("d"); - + const [showDosageDialog, setShowDosageDialog] = useState(false); + const desktopLayout = useBreakpoints({ lg: true, default: false }); const as_needed_boolean = dosageInstruction?.as_needed_boolean; const isFrequencySet = dosageInstruction?.timing?.repeat?.frequency; + + const DosageDialog = () => { + const [localDoseRange, setLocalDoseRange] = useState({ + low: dosageInstruction?.dose_and_rate?.dose_range?.low || { + value: undefined, + unit: undefined, + }, + high: dosageInstruction?.dose_and_rate?.dose_range?.high || { + value: undefined, + unit: undefined, + }, + }); + + return ( +
+
{t("taper_titrate_dosage")}
+
+ + { + setLocalDoseRange((prev) => ({ + ...prev, + low: { + value: value.value, + unit: value.unit as (typeof DOSAGE_UNITS)[number], + }, + high: { + ...prev.high, + unit: value.unit as (typeof DOSAGE_UNITS)[number], + }, + })); + }} + disabled={disabled} + /> +
+
+ + { + setLocalDoseRange((prev) => ({ + ...prev, + high: { + value: value.value, + unit: value.unit as (typeof DOSAGE_UNITS)[number], + }, + low: { + ...prev.low, + unit: value.unit as (typeof DOSAGE_UNITS)[number], + }, + })); + }} + disabled={disabled || !localDoseRange.low.value} + /> +
+
+ + +
+
+ ); + }; + + const formatDoseRange = (range?: DoseRange) => { + if (!range?.low?.value) return ""; + + const lowPart = range.low.unit + ? `${range.low.value} ${range.low.unit}` + : `${range.low.value}`; + const highPart = range.high?.value + ? range.high.unit + ? `${range.high.value} ${range.high.unit}` + : `${range.high.value}` + : ""; + + return highPart ? `${lowPart} → ${highPart}` : lowPart; + }; + return (
{/* Medicine Name and Controls */} @@ -355,26 +479,92 @@ const MedicationRequestGridRow: React.FC<{
{/* Main Fields */} -
+
{/* Dosage */}
- - handleUpdateDosageInstruction({ - dose_and_rate: { type: "ordered", dose_quantity: value }, - }) - } - disabled={disabled} - autoFocus={true} - /> +
+ {dosageInstruction?.dose_and_rate?.dose_range ? ( + setShowDosageDialog(true)} + className="h-9 text-sm cursor-pointer mb-3" + /> + ) : ( + <> + { + handleUpdateDosageInstruction({ + dose_and_rate: { + type: "ordered", + dose_quantity: { + value: value.value, + unit: value.unit as (typeof DOSAGE_UNITS)[number], + }, + dose_range: undefined, + }, + }); + }} + disabled={disabled} + autoFocus={true} + /> +
+ +
+ + )} +
+ + {desktopLayout ? ( + + +
+ + + + + + ) : ( + + + + + + )}
{/* Frequency */} @@ -400,26 +590,31 @@ const MedicationRequestGridRow: React.FC<{ timing: { repeat: { ...dosageInstruction?.timing?.repeat, - ...FREQUENCY_OPTIONS[ - value as keyof typeof FREQUENCY_OPTIONS + ...MEDICATION_REQUEST_TIMING_OPTIONS[ + value as keyof typeof MEDICATION_REQUEST_TIMING_OPTIONS ].timing.repeat, }, + code: MEDICATION_REQUEST_TIMING_OPTIONS[ + value as keyof typeof MEDICATION_REQUEST_TIMING_OPTIONS + ].timing.code, }, }); } }} disabled={disabled} > - + {t("as_needed_prn")} - {Object.entries(FREQUENCY_OPTIONS).map(([key, option]) => ( - - {option.display} - - ))} + {Object.entries(MEDICATION_REQUEST_TIMING_OPTIONS).map( + ([key, option]) => ( + + {option.display} + + ), + )}
@@ -429,7 +624,7 @@ const MedicationRequestGridRow: React.FC<{ - - + { - return Object.entries(FREQUENCY_OPTIONS).find( - ([, value]) => - value.timing.repeat.frequency === option?.repeat?.frequency && - value.timing.repeat.period_unit === option?.repeat?.period_unit && - value.timing.repeat.period === option?.repeat?.period, - )?.[0] as keyof typeof FREQUENCY_OPTIONS; + return Object.entries(MEDICATION_REQUEST_TIMING_OPTIONS).find( + ([key]) => key === option?.code?.code, + )?.[0] as keyof typeof MEDICATION_REQUEST_TIMING_OPTIONS; }; - -// TODO: verify period_unit is correct -const FREQUENCY_OPTIONS = { - BID: { - display: "Two times a day", - timing: { repeat: { frequency: 2, period: 1, period_unit: "d" } }, - }, - TID: { - display: "Three times a day", - timing: { repeat: { frequency: 3, period: 1, period_unit: "d" } }, - }, - QID: { - display: "Four times a day", - timing: { repeat: { frequency: 4, period: 1, period_unit: "d" } }, - }, - AM: { - display: "Every morning", - timing: { repeat: { frequency: 1, period: 1, period_unit: "d" } }, - }, - PM: { - display: "Every afternoon", - timing: { repeat: { frequency: 1, period: 1, period_unit: "d" } }, - }, - QD: { - display: "Every day", - timing: { repeat: { frequency: 1, period: 1, period_unit: "d" } }, - }, - QOD: { - display: "Every other day", - timing: { repeat: { frequency: 1, period: 2, period_unit: "d" } }, - }, - Q1H: { - display: "Every hour", - timing: { repeat: { frequency: 24, period: 1, period_unit: "d" } }, - }, - Q2H: { - display: "Every 2 hours", - timing: { repeat: { frequency: 12, period: 1, period_unit: "d" } }, - }, - Q3H: { - display: "Every 3 hours", - timing: { repeat: { frequency: 8, period: 1, period_unit: "d" } }, - }, - Q4H: { - display: "Every 4 hours", - timing: { repeat: { frequency: 6, period: 1, period_unit: "d" } }, - }, - Q6H: { - display: "Every 6 hours", - timing: { repeat: { frequency: 4, period: 1, period_unit: "d" } }, - }, - Q8H: { - display: "Every 8 hours", - timing: { repeat: { frequency: 3, period: 1, period_unit: "d" } }, - }, - BED: { - display: "At bedtime", - timing: { repeat: { frequency: 1, period: 1, period_unit: "d" } }, - }, - WK: { - display: "Weekly", - timing: { repeat: { frequency: 1, period: 1, period_unit: "wk" } }, - }, - MO: { - display: "Monthly", - timing: { repeat: { frequency: 1, period: 1, period_unit: "mo" } }, - }, - STAT: { - display: "Immediately", - timing: { repeat: { frequency: 1, period: 1, period_unit: "s" } }, // One-time - }, -} as const satisfies Record< - string, - { - display: string; - timing: MedicationRequest["dosage_instruction"][0]["timing"]; - } ->; diff --git a/src/pages/Encounters/PrintPrescription.tsx b/src/pages/Encounters/PrintPrescription.tsx index b5f8f8ec4e6..afd1c345ce2 100644 --- a/src/pages/Encounters/PrintPrescription.tsx +++ b/src/pages/Encounters/PrintPrescription.tsx @@ -5,32 +5,35 @@ import { useTranslation } from "react-i18next"; import PrintPreview from "@/CAREUI/misc/PrintPreview"; -import { Badge } from "@/components/ui/badge"; import { Separator } from "@/components/ui/separator"; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@/components/ui/table"; + +import { reverseFrequencyOption } from "@/components/Questionnaire/QuestionTypes/MedicationRequestQuestion"; + +import { MEDICATION_REQUEST_TIMING_OPTIONS } from "@/common/constants"; import api from "@/Utils/request/api"; import query from "@/Utils/request/query"; import { formatPatientAge } from "@/Utils/utils"; import { MedicationRequest } from "@/types/emr/medicationRequest"; -const FREQUENCY_DISPLAY: Record = { - "1-1-d": { code: "OD", meaning: "Once daily" }, - "2-1-d": { code: "BD", meaning: "Twice daily" }, - "1-1-wk": { code: "QWK", meaning: "Once a week" }, - "4-1-h": { code: "Q4H", meaning: "Every 4 hours" }, - "6-1-h": { code: "QID", meaning: "Four times a day" }, - "8-1-h": { code: "TID", meaning: "Three times a day" }, - "1-1-s": { code: "STAT", meaning: "Immediately" }, - "1-1-d-night": { code: "HS", meaning: "At bedtime" }, - "2-1-d-alt": { code: "QOD", meaning: "Every other day" }, -}; - function getFrequencyDisplay( timing?: MedicationRequest["dosage_instruction"][0]["timing"], ) { - if (!timing?.repeat) return undefined; - const key = `${timing.repeat.frequency}-${timing.repeat.period}-${timing.repeat.period_unit}`; - return FREQUENCY_DISPLAY[key]; + if (!timing) return undefined; + const code = reverseFrequencyOption(timing); + if (!code) return undefined; + return { + code, + meaning: MEDICATION_REQUEST_TIMING_OPTIONS[code].display, + }; } // Helper function to format dosage in Rx style @@ -50,10 +53,7 @@ function formatDosage(instruction: MedicationRequest["dosage_instruction"][0]) { } // Helper function to format dosage instructions in Rx style -function formatSig( - instruction: MedicationRequest["dosage_instruction"][0], - frequency?: { code: string; meaning: string }, -) { +function formatSig(instruction: MedicationRequest["dosage_instruction"][0]) { const parts: string[] = []; // Add route if present @@ -71,16 +71,6 @@ function formatSig( parts.push(`to ${instruction.site.display}`); } - // Add frequency - if (frequency) { - parts.push(frequency.code); - } else if (instruction.timing?.repeat) { - const { frequency, period_unit } = instruction.timing.repeat; - if (frequency) { - parts.push(`${frequency} time(s) per ${period_unit}`); - } - } - return parts.join(" "); } @@ -115,17 +105,6 @@ export const PrintPrescription = (props: { (m) => m.dosage_instruction[0]?.as_needed_boolean, ); - // Collect all unique frequencies used in the prescription - const usedFrequencies = new Set(); - medications?.results?.forEach((med) => { - const timing = med.dosage_instruction[0]?.timing; - if (!timing?.repeat) return; - const key = `${timing.repeat.frequency}-${timing.repeat.period}-${timing.repeat.period_unit}`; - if (FREQUENCY_DISPLAY[key]) { - usedFrequencies.add(key); - } - }); - if (!medications?.results?.length) { return (
@@ -143,7 +122,7 @@ export const PrintPrescription = (props: { } disabled={!(encounter?.patient && medications)} > -
+
{/* Header */}
@@ -184,66 +163,101 @@ export const PrintPrescription = (props: { )}
- {/* Frequency Legend */} - {usedFrequencies.size > 0 && ( -
-

- Frequency Guide: -

-
- {Array.from(usedFrequencies).map((key) => ( -
- - {FREQUENCY_DISPLAY[key].code} - - - = {FREQUENCY_DISPLAY[key].meaning} - -
- ))} -
-
- )} - - {/* Rx Symbol and Medications */} -
-
- - -
- - {/* Normal Medications */} - {normalMedications && normalMedications.length > 0 && ( -
- {normalMedications.map((medication, index) => ( - - ))} -
- )} - - {/* PRN Medications */} - {prnMedications && prnMedications.length > 0 && ( -
-
-

- Take When Required (PRN) -

- -
- {prnMedications.map((medication, index) => ( - - ))} -
- )} + {/* Prescription Table */} +
+

+ PRESCRIPTION +

+ + + + + # + + + Medicine + + + Dose + + + Frequency + + + Duration + + + Remarks + + + + + {normalMedications?.map((medication, index) => { + const instruction = medication.dosage_instruction[0]; + const frequency = getFrequencyDisplay(instruction?.timing); + const dosage = formatDosage(instruction); + const duration = instruction?.timing?.repeat?.bounds_duration; + const remarks = formatSig(instruction); + + return ( + + + {index + 1} + + +
+ {medication.medication?.display} +
+
+ {dosage} + + {frequency?.meaning} + {instruction?.additional_instruction?.[0]?.display && ( +
+ {instruction.additional_instruction[0].display} +
+ )} +
+ + {duration ? `${duration.value} ${duration.unit}` : "-"} + + + {remarks || "-"} + +
+ ); + })} + + {/* PRN Medications */} + {prnMedications?.map((medication, index) => { + const instruction = medication.dosage_instruction[0]; + const dosage = formatDosage(instruction); + const remarks = + instruction?.as_needed_for?.display || "As needed (PRN)"; + + return ( + + + {(normalMedications?.length || 0) + index + 1} + + +
+ {medication.medication?.display} +
+
+ {dosage} + + {t("as_needed_prn")} + + + + {remarks} + +
+ ); + })} +
+
{/* Footer */} @@ -251,7 +265,6 @@ export const PrintPrescription = (props: {
-

Sign of the Consulting Doctor

@@ -290,94 +303,3 @@ const PatientDetail = ({
); }; - -const PrescriptionEntry = ({ - medication, - index, -}: { - medication: MedicationRequest; - index: number; - prn?: boolean; -}) => { - const instruction = medication.dosage_instruction[0]; - - if (!instruction) return null; - - const frequency = getFrequencyDisplay(instruction.timing); - const dosage = formatDosage(instruction); - const sig = formatSig(instruction, frequency); - - const hasAdditionalInstructions = - (instruction.additional_instruction && - instruction.additional_instruction.length > 0) || - medication.note; - - return ( -
-
- {index} -
- - {/* Medicine Name and Status */} -
-
-
-

- {medication.medication?.display} -

-

- {medication.medication?.code} ({medication.medication?.system}) -

-
-
- - {medication.status} - -
-
- - {/* Dosage and Instructions */} -
-
- Dosage: - {dosage} -
-
- Instructions: - {sig} -
- {instruction.as_needed_boolean && ( -
- Take when: - - {instruction.as_needed_for?.display || "As needed (PRN)"} - -
- )} -
- - {/* Additional Instructions */} - {hasAdditionalInstructions && ( -
- {instruction.additional_instruction?.map((instr, idx) => ( -
- {instr.display} -
- ))} - {medication.note && ( -
- - {medication.note} -
- )} -
- )} -
-
- ); -}; From 4f03293fc8b6cd26604b47ddf61a34c1e44c1560 Mon Sep 17 00:00:00 2001 From: Amjith Titus Date: Tue, 14 Jan 2025 20:43:15 +0530 Subject: [PATCH 2/5] Fix dose type --- .../MedicationRequestQuestion.tsx | 18 ++++------------- src/types/emr/medicationRequest.ts | 20 +++++++------------ 2 files changed, 11 insertions(+), 27 deletions(-) diff --git a/src/components/Questionnaire/QuestionTypes/MedicationRequestQuestion.tsx b/src/components/Questionnaire/QuestionTypes/MedicationRequestQuestion.tsx index d46505aed73..4b7c4ca1fff 100644 --- a/src/components/Questionnaire/QuestionTypes/MedicationRequestQuestion.tsx +++ b/src/components/Questionnaire/QuestionTypes/MedicationRequestQuestion.tsx @@ -434,7 +434,7 @@ const MedicationRequestGridRow: React.FC<{ onClick={() => { handleUpdateDosageInstruction({ dose_and_rate: { - type: "calculated", + type: "ordered", dose_range: localDoseRange, }, }); @@ -455,18 +455,8 @@ const MedicationRequestGridRow: React.FC<{ }; const formatDoseRange = (range?: DoseRange) => { - if (!range?.low?.value) return ""; - - const lowPart = range.low.unit - ? `${range.low.value} ${range.low.unit}` - : `${range.low.value}`; - const highPart = range.high?.value - ? range.high.unit - ? `${range.high.value} ${range.high.unit}` - : `${range.high.value}` - : ""; - - return highPart ? `${lowPart} → ${highPart}` : lowPart; + if (!range?.high?.value) return ""; + return `${range?.low?.value} ${range?.low?.unit} → ${range?.high?.value} ${range?.high?.unit}`; }; return ( @@ -525,7 +515,7 @@ const MedicationRequestGridRow: React.FC<{ dosageInstruction?.dose_and_rate?.dose_quantity?.value; handleUpdateDosageInstruction({ dose_and_rate: { - type: "calculated", + type: "ordered", dose_quantity: undefined, dose_range: { low: { diff --git a/src/types/emr/medicationRequest.ts b/src/types/emr/medicationRequest.ts index 5a528bb3ed7..5e11ad40d08 100644 --- a/src/types/emr/medicationRequest.ts +++ b/src/types/emr/medicationRequest.ts @@ -121,20 +121,14 @@ export interface MedicationRequestDosageInstruction { * One of `dose_quantity` or `dose_range` must be present. * `type` is optional and defaults to `ordered`. * - * - If `type` is `ordered`, `dose_quantity` must be present. - * - If `type` is `calculated`, `dose_range` must be present. This is used for titrated medications. + * - If `type` is `ordered`, the dose specified is as ordered by the prescriber. + * - If `type` is `calculated`, the dose specified is calculated by the prescriber or the system. */ - dose_and_rate?: - | { - type?: "ordered"; - dose_quantity?: DosageQuantity; - dose_range?: undefined; - } - | { - type: "calculated"; - dose_range?: DoseRange; - dose_quantity?: undefined; - }; + dose_and_rate?: { + type: "ordered" | "calculated"; + dose_quantity?: DosageQuantity; + dose_range?: DoseRange; + }; max_dose_per_period?: DoseRange; } From 3316ce73def577fdbfe44f9bc061e30e444fba39 Mon Sep 17 00:00:00 2001 From: Amjith Titus Date: Tue, 14 Jan 2025 20:53:33 +0530 Subject: [PATCH 3/5] Fix Scroll Issue --- src/components/Questionnaire/QuestionTypes/QuestionInput.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/components/Questionnaire/QuestionTypes/QuestionInput.tsx b/src/components/Questionnaire/QuestionTypes/QuestionInput.tsx index b6a328458b3..87c913dbee7 100644 --- a/src/components/Questionnaire/QuestionTypes/QuestionInput.tsx +++ b/src/components/Questionnaire/QuestionTypes/QuestionInput.tsx @@ -177,7 +177,8 @@ export function QuestionInput({ > {index === 0 && }
From 18efb6ecca909ee7629d867383a5b4fbe8ffda16 Mon Sep 17 00:00:00 2001 From: Bodhish Thomas Date: Tue, 14 Jan 2025 22:44:22 +0530 Subject: [PATCH 4/5] add medication request questionnaire to structured form data --- .../Questionnaire/data/StructuredFormData.tsx | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/src/components/Questionnaire/data/StructuredFormData.tsx b/src/components/Questionnaire/data/StructuredFormData.tsx index 757b4a688f1..7a5aca3c248 100644 --- a/src/components/Questionnaire/data/StructuredFormData.tsx +++ b/src/components/Questionnaire/data/StructuredFormData.tsx @@ -20,6 +20,27 @@ const encounterQuestionnaire: QuestionnaireDetail = { tags: [], }; +const medication_request_questionnaire: QuestionnaireDetail = { + id: "medication_request", + slug: "medication_request", + version: "0.0.1", + title: "Medication Request", + status: "active", + subject_type: "patient", + questions: [ + { + id: "medication_request", + text: "Medication Request", + type: "structured", + structured_type: "medication_request", + link_id: "1.1", + required: true, + }, + ], + tags: [], +}; + export const FIXED_QUESTIONNAIRES: Record = { encounter: encounterQuestionnaire, + medication_request: medication_request_questionnaire, }; From 0230f5c2e0dbc69965bb4a66e29d5ad0e4ea1832 Mon Sep 17 00:00:00 2001 From: Bodhish Thomas Date: Wed, 15 Jan 2025 03:23:39 +0530 Subject: [PATCH 5/5] Clean up medication request type --- src/common/constants.tsx | 198 ------ .../Common/ComboboxQuantityInput.tsx | 75 +-- .../MedicineAdministrationSheet/index.tsx | 5 +- .../MedicationRequestQuestion.tsx | 628 +++++++++--------- .../MedicationStatementQuestion.tsx | 3 +- src/pages/Encounters/PrintPrescription.tsx | 13 +- src/types/emr/medicationRequest.ts | 424 +++++++++++- src/types/emr/medicationStatement.ts | 20 +- 8 files changed, 767 insertions(+), 599 deletions(-) diff --git a/src/common/constants.tsx b/src/common/constants.tsx index 7db067922e0..a88b50e35b0 100644 --- a/src/common/constants.tsx +++ b/src/common/constants.tsx @@ -1,7 +1,5 @@ import { IconName } from "@/CAREUI/icons/CareIcon"; -import { MedicationRequest } from "@/types/emr/medicationRequest"; - export const RESULTS_PER_PAGE_LIMIT = 14; /** @@ -911,199 +909,3 @@ export const HEADER_CONTENT_TYPES = { } as const; export const ADMIN_USER_TYPES = ["DistrictAdmin", "StateAdmin"] as const; - -export const MEDICATION_REQUEST_TIMING_OPTIONS = { - BID: { - display: "BID (1-0-1)", - timing: { - repeat: { frequency: 2, period: 1, period_unit: "d" }, - code: { - code: "BID", - display: "Two times a day", - system: "http://terminology.hl7.org/CodeSystem/v3-GTSAbbreviation", - }, - }, - }, - TID: { - display: "TID (1-1-1)", - timing: { - repeat: { frequency: 3, period: 1, period_unit: "d" }, - code: { - code: "TID", - display: "Three times a day", - system: "http://terminology.hl7.org/CodeSystem/v3-GTSAbbreviation", - }, - }, - }, - QID: { - display: "QID (1-1-1-1)", - timing: { - repeat: { frequency: 4, period: 1, period_unit: "d" }, - code: { - code: "QID", - display: "Four times a day", - system: "http://terminology.hl7.org/CodeSystem/v3-GTSAbbreviation", - }, - }, - }, - AM: { - display: "AM (1-0-0)", - timing: { - repeat: { frequency: 1, period: 1, period_unit: "d" }, - code: { - code: "AM", - display: "Every morning", - system: "http://terminology.hl7.org/CodeSystem/v3-GTSAbbreviation", - }, - }, - }, - PM: { - display: "PM (0-0-1)", - timing: { - repeat: { frequency: 1, period: 1, period_unit: "d" }, - code: { - code: "PM", - display: "Every afternoon", - system: "http://terminology.hl7.org/CodeSystem/v3-GTSAbbreviation", - }, - }, - }, - QD: { - display: "QD (Once a day)", - timing: { - repeat: { frequency: 1, period: 1, period_unit: "d" }, - code: { - code: "QD", - display: "Once a day", - system: "http://terminology.hl7.org/CodeSystem/v3-GTSAbbreviation", - }, - }, - }, - QOD: { - display: "QOD (Alternate days)", - timing: { - repeat: { frequency: 1, period: 2, period_unit: "d" }, - code: { - code: "QOD", - display: "Alternate days", - system: "http://terminology.hl7.org/CodeSystem/v3-GTSAbbreviation", - }, - }, - }, - Q1H: { - display: "Q1H (Every 1 hour)", - timing: { - repeat: { frequency: 1, period: 1, period_unit: "h" }, - code: { - code: "Q1H", - display: "Every 1 hour", - system: "http://terminology.hl7.org/CodeSystem/v3-GTSAbbreviation", - }, - }, - }, - Q2H: { - display: "Q2H (Every 2 hours)", - timing: { - repeat: { frequency: 1, period: 2, period_unit: "h" }, - code: { - code: "Q2H", - display: "Every 2 hours", - system: "http://terminology.hl7.org/CodeSystem/v3-GTSAbbreviation", - }, - }, - }, - Q3H: { - display: "Q3H (Every 3 hours)", - timing: { - repeat: { frequency: 1, period: 3, period_unit: "h" }, - code: { - code: "Q3H", - display: "Every 3 hours", - system: "http://terminology.hl7.org/CodeSystem/v3-GTSAbbreviation", - }, - }, - }, - Q4H: { - display: "Q4H (Every 4 hours)", - timing: { - repeat: { frequency: 1, period: 4, period_unit: "h" }, - code: { - code: "Q4H", - display: "Every 4 hours", - system: "http://terminology.hl7.org/CodeSystem/v3-GTSAbbreviation", - }, - }, - }, - Q6H: { - display: "Q6H (Every 6 hours)", - timing: { - repeat: { frequency: 1, period: 6, period_unit: "h" }, - code: { - code: "Q6H", - display: "Every 6 hours", - system: "http://terminology.hl7.org/CodeSystem/v3-GTSAbbreviation", - }, - }, - }, - Q8H: { - display: "Q8H (Every 8 hours)", - timing: { - repeat: { frequency: 1, period: 8, period_unit: "h" }, - code: { - code: "Q8H", - display: "Every 8 hours", - system: "http://terminology.hl7.org/CodeSystem/v3-GTSAbbreviation", - }, - }, - }, - BED: { - display: "BED (0-0-1)", - timing: { - repeat: { frequency: 1, period: 1, period_unit: "d" }, - code: { - code: "BED", - display: "Bedtime", - system: "http://terminology.hl7.org/CodeSystem/v3-GTSAbbreviation", - }, - }, - }, - WK: { - display: "WK (Weekly)", - timing: { - repeat: { frequency: 1, period: 1, period_unit: "wk" }, - code: { - code: "WK", - display: "Weekly", - system: "http://terminology.hl7.org/CodeSystem/v3-GTSAbbreviation", - }, - }, - }, - MO: { - display: "MO (Monthly)", - timing: { - repeat: { frequency: 1, period: 1, period_unit: "mo" }, - code: { - code: "MO", - display: "Monthly", - system: "http://terminology.hl7.org/CodeSystem/v3-GTSAbbreviation", - }, - }, - }, - STAT: { - display: "STAT (Immediately)", - timing: { - repeat: { frequency: 1, period: 1, period_unit: "s" }, - code: { - code: "STAT", - display: "Immediately", - system: "http://terminology.hl7.org/CodeSystem/v3-GTSAbbreviation", - }, - }, - }, -} as const satisfies Record< - string, - { - display: string; - timing: MedicationRequest["dosage_instruction"][0]["timing"]; - } ->; diff --git a/src/components/Common/ComboboxQuantityInput.tsx b/src/components/Common/ComboboxQuantityInput.tsx index 65f386dad08..20cb540ed13 100644 --- a/src/components/Common/ComboboxQuantityInput.tsx +++ b/src/components/Common/ComboboxQuantityInput.tsx @@ -1,6 +1,5 @@ "use client"; -import { t } from "i18next"; import { Check } from "lucide-react"; import * as React from "react"; @@ -20,35 +19,31 @@ import { PopoverTrigger, } from "@/components/ui/popover"; -interface Props { - quantity?: QuantityValue | null; - onChange: (quantity: QuantityValue) => void; - units: readonly TUnit[]; +import { + DOSAGE_UNITS_CODES, + DosageQuantity, +} from "@/types/emr/medicationRequest"; + +interface Props { + quantity?: DosageQuantity; + onChange: (quantity: DosageQuantity) => void; disabled?: boolean; placeholder?: string; autoFocus?: boolean; } -interface QuantityValue { - value?: number; - unit?: TUnit; -} - -export function ComboboxQuantityInput({ - quantity = { value: undefined, unit: undefined }, +export function ComboboxQuantityInput({ + quantity, onChange, - units, disabled, placeholder = "Enter a number...", autoFocus, -}: Props) { +}: Props) { const [open, setOpen] = React.useState(false); const [inputValue, setInputValue] = React.useState( - quantity?.value?.toString() || "", - ); - const [selectedUnit, setSelectedUnit] = React.useState( - quantity?.unit, + quantity?.value.toString() || "", ); + const [selectedUnit, setSelectedUnit] = React.useState(quantity?.unit); const inputRef = React.useRef(null); const [activeIndex, setActiveIndex] = React.useState(-1); @@ -59,12 +54,13 @@ export function ComboboxQuantityInput({ if (value === "" || /^\d+$/.test(value)) { setInputValue(value); setOpen(true); - setSelectedUnit(undefined); setActiveIndex(0); - onChange({ - value: value ? parseInt(value, 10) : undefined, - unit: selectedUnit, - }); + if (value && selectedUnit) { + onChange({ + value: parseInt(value, 10), + unit: selectedUnit, + }); + } } }; @@ -75,15 +71,19 @@ export function ComboboxQuantityInput({ e.preventDefault(); setOpen(true); setActiveIndex((prev) => - prev === -1 ? 0 : prev < units.length - 1 ? prev + 1 : prev, + prev === -1 + ? 0 + : prev < DOSAGE_UNITS_CODES.length - 1 + ? prev + 1 + : prev, ); } else if (e.key === "ArrowUp") { e.preventDefault(); setActiveIndex((prev) => (prev > 0 ? prev - 1 : prev)); } else if (e.key === "Enter") { e.preventDefault(); - if (activeIndex >= 0 && activeIndex < units.length) { - const unit = units[activeIndex]; + if (activeIndex >= 0 && activeIndex < DOSAGE_UNITS_CODES.length) { + const unit = DOSAGE_UNITS_CODES[activeIndex]; setSelectedUnit(unit); setOpen(false); setActiveIndex(-1); @@ -92,15 +92,6 @@ export function ComboboxQuantityInput({ } }; - React.useEffect(() => { - if (quantity?.value !== undefined) { - setInputValue(quantity.value.toString()); - } - if (quantity?.unit !== undefined) { - setSelectedUnit(quantity.unit); - } - }, [quantity]); - return (
@@ -121,7 +112,7 @@ export function ComboboxQuantityInput({ /> {selectedUnit && (
- {t(`unit_${selectedUnit}`)} + {selectedUnit.display}
)}
@@ -138,10 +129,10 @@ export function ComboboxQuantityInput({ No results found. - {units.map((unit, index) => ( + {DOSAGE_UNITS_CODES.map((unit, index) => ( { setSelectedUnit(unit); setOpen(false); @@ -155,12 +146,14 @@ export function ComboboxQuantityInput({ )} >
- {inputValue} {t(`unit_${unit}`)} + {inputValue} {unit.display}
diff --git a/src/components/Medicine/MedicineAdministrationSheet/index.tsx b/src/components/Medicine/MedicineAdministrationSheet/index.tsx index aa894b8d5c2..166c22016cb 100644 --- a/src/components/Medicine/MedicineAdministrationSheet/index.tsx +++ b/src/components/Medicine/MedicineAdministrationSheet/index.tsx @@ -284,7 +284,8 @@ const PrescriptionEntry = ({
{instruction.dose_and_rate && ( - {instruction.dose_and_rate.type === "calculated" ? ( + {/* TODO: Rebuild Medicine Administration Sheet */} + {/* {instruction.dose_and_rate.type === "calculated" ? ( {instruction.dose_and_rate.dose_range?.low.value}{" "} {instruction.dose_and_rate.dose_range?.low.unit} →{" "} @@ -296,7 +297,7 @@ const PrescriptionEntry = ({ {instruction.dose_and_rate.dose_quantity?.value}{" "} {instruction.dose_and_rate.dose_quantity?.unit} - )} + )} */} )} {instruction.route && ( diff --git a/src/components/Questionnaire/QuestionTypes/MedicationRequestQuestion.tsx b/src/components/Questionnaire/QuestionTypes/MedicationRequestQuestion.tsx index 4b7c4ca1fff..73361b3e774 100644 --- a/src/components/Questionnaire/QuestionTypes/MedicationRequestQuestion.tsx +++ b/src/components/Questionnaire/QuestionTypes/MedicationRequestQuestion.tsx @@ -41,17 +41,15 @@ import ValueSetSelect from "@/components/Questionnaire/ValueSetSelect"; import useBreakpoints from "@/hooks/useBreakpoints"; -import { MEDICATION_REQUEST_TIMING_OPTIONS } from "@/common/constants"; - import { - BOUNDS_DURATION_UNITS, - BoundsDuration, - DOSAGE_UNITS, DoseRange, MEDICATION_REQUEST_INTENT, + MEDICATION_REQUEST_TIMING_OPTIONS, MedicationRequest, MedicationRequestDosageInstruction, MedicationRequestIntent, + UCUM_TIME_UNITS, + parseMedicationStringToRequest, } from "@/types/emr/medicationRequest"; import { Code } from "@/types/questionnaire/code"; import { QuestionnaireResponse } from "@/types/questionnaire/form"; @@ -62,17 +60,6 @@ interface MedicationRequestQuestionProps { disabled?: boolean; } -const MEDICATION_REQUEST_INITIAL_VALUE: MedicationRequest = { - status: "active", - intent: "order", - category: "inpatient", - priority: "urgent", - do_not_perform: false, - medication: undefined, - authored_on: new Date().toISOString(), - dosage_instruction: [], -}; - export function MedicationRequestQuestion({ questionnaireResponse, updateQuestionnaireResponseCB, @@ -80,9 +67,11 @@ export function MedicationRequestQuestion({ }: MedicationRequestQuestionProps) { const medications = (questionnaireResponse.values?.[0]?.value as MedicationRequest[]) || []; + const [expandedMedicationIndex, setExpandedMedicationIndex] = useState< number | null >(null); + const [medicationToDelete, setMedicationToDelete] = useState( null, ); @@ -92,9 +81,7 @@ export function MedicationRequestQuestion({ const newMedications: MedicationRequest[] = [ ...medications, { - ...MEDICATION_REQUEST_INITIAL_VALUE, - medication, - dosage_instruction: [], + ...parseMedicationStringToRequest(medication), }, ]; updateQuestionnaireResponseCB({ @@ -236,7 +223,7 @@ export function MedicationRequestQuestion({ >
{ e.stopPropagation(); handleRemoveMedication(index); }} disabled={disabled} + className="h-8 w-8" > @@ -333,16 +320,23 @@ export function MedicationRequestQuestion({ ); } -const MedicationRequestGridRow: React.FC<{ +interface MedicationRequestGridRowProps { medication: MedicationRequest; disabled?: boolean; onUpdate?: (medication: Partial) => void; onRemove?: () => void; -}> = ({ medication, disabled, onUpdate, onRemove }) => { - const dosageInstruction = - medication.dosage_instruction.length > 0 - ? medication.dosage_instruction[0] - : undefined; +} + +const MedicationRequestGridRow: React.FC = ({ + medication, + disabled, + onUpdate, + onRemove, +}) => { + const [showDosageDialog, setShowDosageDialog] = useState(false); + const desktopLayout = useBreakpoints({ lg: true, default: false }); + const dosageInstruction = medication.dosage_instruction[0]; + const handleUpdateDosageInstruction = ( updates: Partial, ) => { @@ -351,23 +345,17 @@ const MedicationRequestGridRow: React.FC<{ }); }; - const [boundsDurationUnit, setBoundsDurationUnit] = useState("d"); - const [showDosageDialog, setShowDosageDialog] = useState(false); - const desktopLayout = useBreakpoints({ lg: true, default: false }); - const as_needed_boolean = dosageInstruction?.as_needed_boolean; - const isFrequencySet = dosageInstruction?.timing?.repeat?.frequency; + const formatDoseRange = (range?: DoseRange) => { + if (!range?.high?.value) return ""; + return `${range.low?.value} ${range.low?.unit?.display} → ${range.high?.value} ${range.high?.unit?.display}`; + }; + interface DosageDialogProps { + dosageRange: DoseRange; + } - const DosageDialog = () => { - const [localDoseRange, setLocalDoseRange] = useState({ - low: dosageInstruction?.dose_and_rate?.dose_range?.low || { - value: undefined, - unit: undefined, - }, - high: dosageInstruction?.dose_and_rate?.dose_range?.high || { - value: undefined, - unit: undefined, - }, - }); + const DosageDialog: React.FC = ({ dosageRange }) => { + const [localDoseRange, setLocalDoseRange] = + useState(dosageRange); return (
@@ -375,18 +363,14 @@ const MedicationRequestGridRow: React.FC<{
{ setLocalDoseRange((prev) => ({ ...prev, - low: { - value: value.value, - unit: value.unit as (typeof DOSAGE_UNITS)[number], - }, + low: value, high: { ...prev.high, - unit: value.unit as (typeof DOSAGE_UNITS)[number], + unit: value.unit, }, })); }} @@ -396,18 +380,14 @@ const MedicationRequestGridRow: React.FC<{
{ setLocalDoseRange((prev) => ({ ...prev, - high: { - value: value.value, - unit: value.unit as (typeof DOSAGE_UNITS)[number], - }, + high: value, low: { ...prev.low, - unit: value.unit as (typeof DOSAGE_UNITS)[number], + unit: value.unit, }, })); }} @@ -419,11 +399,7 @@ const MedicationRequestGridRow: React.FC<{ variant="outline" onClick={() => { handleUpdateDosageInstruction({ - dose_and_rate: { - type: "ordered", - dose_quantity: undefined, - dose_range: undefined, - }, + dose_and_rate: undefined, }); setShowDosageDialog(false); }} @@ -454,322 +430,326 @@ const MedicationRequestGridRow: React.FC<{ ); }; - const formatDoseRange = (range?: DoseRange) => { - if (!range?.high?.value) return ""; - return `${range?.low?.value} ${range?.low?.unit} → ${range?.high?.value} ${range?.high?.unit}`; + const handleDoseRangeClick = () => { + const dose_quantity = dosageInstruction?.dose_and_rate?.dose_quantity; + + if (dose_quantity) { + handleUpdateDosageInstruction({ + dose_and_rate: { + type: "ordered", + dose_quantity: undefined, + dose_range: { + low: dose_quantity, + high: dose_quantity, + }, + }, + }); + } + setShowDosageDialog(true); }; return (
- {/* Medicine Name and Controls */} + {/* Medicine Name */}
{medication.medication?.display}
- - {/* Main Fields */} -
- {/* Dosage */} -
- -
- {dosageInstruction?.dose_and_rate?.dose_range ? ( - setShowDosageDialog(true)} - className="h-9 text-sm cursor-pointer mb-3" - /> - ) : ( - <> - { - handleUpdateDosageInstruction({ - dose_and_rate: { - type: "ordered", - dose_quantity: { - value: value.value, - unit: value.unit as (typeof DOSAGE_UNITS)[number], - }, - dose_range: undefined, + {/* Dosage */} +
+ +
+ {dosageInstruction?.dose_and_rate?.dose_range ? ( + setShowDosageDialog(true)} + className="h-9 text-sm cursor-pointer mb-3" + /> + ) : ( + <> + { + if (!value.value || !value.unit) return; + handleUpdateDosageInstruction({ + dose_and_rate: { + type: "ordered", + dose_quantity: { + value: value.value, + unit: value.unit, }, - }); - }} - disabled={disabled} - autoFocus={true} - /> -
- -
- - )} -
+ dose_range: undefined, + }, + }); + }} + disabled={disabled} + /> +
+ +
+ + )} +
- {desktopLayout ? ( + {dosageInstruction?.dose_and_rate?.dose_range && + (desktopLayout ? (
- + ) : ( - + - )} -
+ ))} +
+ {/* Frequency */} +
+ + { - if (value === "PRN") { - handleUpdateDosageInstruction({ - as_needed_boolean: true, - timing: undefined, - }); - } else { + }} + disabled={disabled} + > + + + + + {t("as_needed_prn")} + {Object.entries(MEDICATION_REQUEST_TIMING_OPTIONS).map( + ([key, option]) => ( + + {option.display} + + ), + )} + + +
+ {/* Duration */} +
+ +
+ {dosageInstruction?.timing && ( + { + const value = e.target.value; + if (!dosageInstruction.timing) return; handleUpdateDosageInstruction({ - as_needed_boolean: false, timing: { + ...dosageInstruction.timing, repeat: { - ...dosageInstruction?.timing?.repeat, - ...MEDICATION_REQUEST_TIMING_OPTIONS[ - value as keyof typeof MEDICATION_REQUEST_TIMING_OPTIONS - ].timing.repeat, + ...dosageInstruction.timing.repeat, + bounds_duration: { + value: Number(value), + unit: dosageInstruction.timing.repeat.bounds_duration + .unit, + }, }, - code: MEDICATION_REQUEST_TIMING_OPTIONS[ - value as keyof typeof MEDICATION_REQUEST_TIMING_OPTIONS - ].timing.code, }, }); + }} + disabled={ + disabled || + !dosageInstruction?.timing?.repeat || + dosageInstruction?.as_needed_boolean } - }} - disabled={disabled} - > - - - - - {t("as_needed_prn")} - {Object.entries(MEDICATION_REQUEST_TIMING_OPTIONS).map( - ([key, option]) => ( - - {option.display} - - ), - )} - - -
- - {/* Duration */} -
- - { - if (value?.unit) { - setBoundsDurationUnit( - value?.unit as (typeof BOUNDS_DURATION_UNITS)[number], - ); - } + className="h-9 text-sm" + /> + )} + - onUpdate?.({ intent: value }) + disabled={ + disabled || + !dosageInstruction?.timing?.repeat || + dosageInstruction?.as_needed_boolean } - disabled={disabled} > - - + + - {MEDICATION_REQUEST_INTENT.map((intent) => ( - - {intent.replace(/_/g, " ")} + {UCUM_TIME_UNITS.map((unit) => ( + + {unit} ))}
- - {/* Remove Button - Desktop */} -
- -
+
+ {/* Instructions */} +
+ + + handleUpdateDosageInstruction({ as_needed_for: reason }) + } + placeholder={t("select_prn_reason")} + disabled={disabled || !dosageInstruction?.as_needed_boolean} + wrapTextForSmallScreen={true} + /> +
+ {/* Additional Instructions */} +
+ + + handleUpdateDosageInstruction({ + additional_instruction: [instruction], + }) + } + placeholder={t("select_additional_instructions")} + disabled={disabled} + /> +
+ {/* Route */} +
+ + handleUpdateDosageInstruction({ route })} + placeholder={t("select_route")} + disabled={disabled} + /> +
+ {/* Site */} +
+ + handleUpdateDosageInstruction({ site })} + placeholder={t("select_site")} + disabled={disabled} + wrapTextForSmallScreen={true} + /> +
+ {/* Method */} +
+ + handleUpdateDosageInstruction({ method })} + placeholder={t("select_method")} + disabled={disabled} + count={20} + /> +
+ {/* Intent */} +
+ + +
+ {/* Remove Button */} +
+
); diff --git a/src/components/Questionnaire/QuestionTypes/MedicationStatementQuestion.tsx b/src/components/Questionnaire/QuestionTypes/MedicationStatementQuestion.tsx index 42321e84b7c..3482310b778 100644 --- a/src/components/Questionnaire/QuestionTypes/MedicationStatementQuestion.tsx +++ b/src/components/Questionnaire/QuestionTypes/MedicationStatementQuestion.tsx @@ -215,7 +215,8 @@ const MedicationStatementItem: React.FC<{ icon: "l-user", }, { - value: MedicationStatementInformationSourceType.USER, + value: + MedicationStatementInformationSourceType.PRACTITIONER, icon: "l-user-nurse", }, { diff --git a/src/pages/Encounters/PrintPrescription.tsx b/src/pages/Encounters/PrintPrescription.tsx index afd1c345ce2..c071091306d 100644 --- a/src/pages/Encounters/PrintPrescription.tsx +++ b/src/pages/Encounters/PrintPrescription.tsx @@ -17,12 +17,13 @@ import { import { reverseFrequencyOption } from "@/components/Questionnaire/QuestionTypes/MedicationRequestQuestion"; -import { MEDICATION_REQUEST_TIMING_OPTIONS } from "@/common/constants"; - import api from "@/Utils/request/api"; import query from "@/Utils/request/query"; import { formatPatientAge } from "@/Utils/utils"; -import { MedicationRequest } from "@/types/emr/medicationRequest"; +import { + MEDICATION_REQUEST_TIMING_OPTIONS, + MedicationRequest, +} from "@/types/emr/medicationRequest"; function getFrequencyDisplay( timing?: MedicationRequest["dosage_instruction"][0]["timing"], @@ -43,13 +44,13 @@ function formatDosage(instruction: MedicationRequest["dosage_instruction"][0]) { if (instruction.dose_and_rate.type === "calculated") { const { dose_range } = instruction.dose_and_rate; if (!dose_range) return ""; - return `${dose_range.low.value}${dose_range.low.unit} - ${dose_range.high.value}${dose_range.high.unit}`; + return `${dose_range.low.value}${dose_range.low.unit.display} - ${dose_range.high.value}${dose_range.high.unit.display}`; } const { dose_quantity } = instruction.dose_and_rate; - if (!dose_quantity?.value) return ""; + if (!dose_quantity?.value || !dose_quantity.unit) return ""; - return `${dose_quantity.value} ${dose_quantity.unit || ""}`.trim(); + return `${dose_quantity.value} ${dose_quantity.unit.display}`; } // Helper function to format dosage instructions in Rx style diff --git a/src/types/emr/medicationRequest.ts b/src/types/emr/medicationRequest.ts index 5e11ad40d08..c102fac90a6 100644 --- a/src/types/emr/medicationRequest.ts +++ b/src/types/emr/medicationRequest.ts @@ -2,24 +2,41 @@ import { UserBareMinimum } from "@/components/Users/models"; import { Code } from "@/types/questionnaire/code"; -export const DOSAGE_UNITS = [ - "mg", - "g", - "ml", - "drop(s)", - "ampule(s)", - "tsp", - "mcg", - "unit(s)", +export const DOSAGE_UNITS_CODES = [ + { + code: "mg", + display: "Milligram", + system: "http://unitsofmeasure.org", + }, + { + code: "g", + display: "Gram", + system: "http://unitsofmeasure.org", + }, + { + code: "mL", + display: "Milliliter", + system: "http://unitsofmeasure.org", + }, + { + code: "[drp]", + display: "Drop", + system: "http://unitsofmeasure.org", + }, + { + code: "{tbl}", + display: "Tablets", + system: "http://unitsofmeasure.org", + }, ] as const; -export const BOUNDS_DURATION_UNITS = [ +export const UCUM_TIME_UNITS = [ // TODO: Are these smaller units required? // "ms", // "s, // "min", - "h", "d", + "h", "wk", "mo", "a", @@ -32,7 +49,7 @@ export const MEDICATION_REQUEST_STATUS = [ "stopped", "completed", "cancelled", - "entered-in-error", + "entered_in_error", "draft", "unknown", ] as const; @@ -73,14 +90,13 @@ export type MedicationRequestIntent = (typeof MEDICATION_REQUEST_INTENT)[number]; export interface DosageQuantity { - value?: number; - // TODO: confirm if we should be using these units itself. - unit?: (typeof DOSAGE_UNITS)[number]; + value: number; + unit: Code; } export interface BoundsDuration { value: number; - unit: (typeof BOUNDS_DURATION_UNITS)[number]; + unit: (typeof UCUM_TIME_UNITS)[number]; } export interface DoseRange { @@ -89,13 +105,13 @@ export interface DoseRange { } export interface Timing { - repeat?: { + repeat: { frequency: number; period: number; - period_unit: "s" | "min" | "h" | "d" | "wk" | "mo" | "a"; - bounds_duration?: BoundsDuration; + period_unit: (typeof UCUM_TIME_UNITS)[number]; + bounds_duration: BoundsDuration; }; - code?: Code; + code: Code; } export interface MedicationRequestDosageInstruction { @@ -109,10 +125,11 @@ export interface MedicationRequestDosageInstruction { /** * True if it is a PRN medication */ - as_needed_boolean?: boolean; + as_needed_boolean: boolean; /** * If it is a PRN medication (as_needed_boolean is true), the indicator. */ + // Todo: Implement a selector for PRN as needed reason, Backend value set: system-as-needed-reason as_needed_for?: Code; site?: Code; route?: Code; @@ -136,18 +153,373 @@ export interface MedicationRequest { readonly id?: string; status?: MedicationRequestStatus; status_reason?: MedicationRequestStatusReason; - status_changed?: string; // DateTime intent?: MedicationRequestIntent; category?: "inpatient" | "outpatient" | "community" | "discharge"; priority?: "stat" | "urgent" | "asap" | "routine"; do_not_perform: boolean; medication?: Code; - patient?: string; // UUID encounter?: string; // UUID - authored_on: string; dosage_instruction: MedicationRequestDosageInstruction[]; note?: string; +} + +export interface MedicationRequestRead { + id: string; + status: MedicationRequestStatus; + status_reason?: MedicationRequestStatusReason; + intent: MedicationRequestIntent; + category: "inpatient" | "outpatient" | "community" | "discharge"; + priority: "stat" | "urgent" | "asap" | "routine"; + do_not_perform: boolean; + medication: Code; + encounter: string; + dosage_instruction: MedicationRequestDosageInstruction[]; + note?: string; + created_date: string; + modified_date: string; + created_by: UserBareMinimum; + updated_by: UserBareMinimum; +} + +export const MEDICATION_REQUEST_TIMING_OPTIONS: Record< + string, + { + display: string; + timing: Timing; + } +> = { + BID: { + display: "BID (1-0-1)", + timing: { + repeat: { + frequency: 2, + period: 1, + period_unit: "d", + bounds_duration: { + value: 1, + unit: "d", + }, + }, + code: { + code: "BID", + display: "Two times a day", + system: "http://terminology.hl7.org/CodeSystem/v3-GTSAbbreviation", + }, + }, + }, + TID: { + display: "TID (1-1-1)", + timing: { + repeat: { + frequency: 3, + period: 1, + period_unit: "d", + bounds_duration: { + value: 1, + unit: "d", + }, + }, + code: { + code: "TID", + display: "Three times a day", + system: "http://terminology.hl7.org/CodeSystem/v3-GTSAbbreviation", + }, + }, + }, + QID: { + display: "QID (1-1-1-1)", + timing: { + repeat: { + frequency: 4, + period: 1, + period_unit: "d", + bounds_duration: { + value: 1, + unit: "d", + }, + }, + code: { + code: "QID", + display: "Four times a day", + system: "http://terminology.hl7.org/CodeSystem/v3-GTSAbbreviation", + }, + }, + }, + AM: { + display: "AM (1-0-0)", + timing: { + repeat: { + frequency: 1, + period: 1, + period_unit: "d", + bounds_duration: { + value: 1, + unit: "d", + }, + }, + code: { + code: "AM", + display: "Every morning", + system: "http://terminology.hl7.org/CodeSystem/v3-GTSAbbreviation", + }, + }, + }, + PM: { + display: "PM (0-0-1)", + timing: { + repeat: { + frequency: 1, + period: 1, + period_unit: "d", + bounds_duration: { + value: 1, + unit: "d", + }, + }, + code: { + code: "PM", + display: "Every afternoon", + system: "http://terminology.hl7.org/CodeSystem/v3-GTSAbbreviation", + }, + }, + }, + QD: { + display: "QD (Once a day)", + timing: { + repeat: { + frequency: 1, + period: 1, + period_unit: "d", + bounds_duration: { + value: 1, + unit: "d", + }, + }, + code: { + code: "QD", + display: "Once a day", + system: "http://terminology.hl7.org/CodeSystem/v3-GTSAbbreviation", + }, + }, + }, + QOD: { + display: "QOD (Alternate days)", + timing: { + repeat: { + frequency: 1, + period: 2, + period_unit: "d", + bounds_duration: { + value: 2, + unit: "d", + }, + }, + code: { + code: "QOD", + display: "Alternate days", + system: "http://terminology.hl7.org/CodeSystem/v3-GTSAbbreviation", + }, + }, + }, + Q1H: { + display: "Q1H (Every 1 hour)", + timing: { + repeat: { + frequency: 1, + period: 1, + period_unit: "h", + bounds_duration: { + value: 1, + unit: "d", + }, + }, + code: { + code: "Q1H", + display: "Every 1 hour", + system: "http://terminology.hl7.org/CodeSystem/v3-GTSAbbreviation", + }, + }, + }, + Q2H: { + display: "Q2H (Every 2 hours)", + timing: { + repeat: { + frequency: 1, + period: 2, + period_unit: "h", + bounds_duration: { + value: 1, + unit: "d", + }, + }, + code: { + code: "Q2H", + display: "Every 2 hours", + system: "http://terminology.hl7.org/CodeSystem/v3-GTSAbbreviation", + }, + }, + }, + Q3H: { + display: "Q3H (Every 3 hours)", + timing: { + repeat: { + frequency: 1, + period: 3, + period_unit: "h", + bounds_duration: { + value: 1, + unit: "d", + }, + }, + code: { + code: "Q3H", + display: "Every 3 hours", + system: "http://terminology.hl7.org/CodeSystem/v3-GTSAbbreviation", + }, + }, + }, + Q4H: { + display: "Q4H (Every 4 hours)", + timing: { + repeat: { + frequency: 1, + period: 4, + period_unit: "h", + bounds_duration: { + value: 1, + unit: "d", + }, + }, + code: { + code: "Q4H", + display: "Every 4 hours", + system: "http://terminology.hl7.org/CodeSystem/v3-GTSAbbreviation", + }, + }, + }, + Q6H: { + display: "Q6H (Every 6 hours)", + timing: { + repeat: { + frequency: 1, + period: 6, + period_unit: "h", + bounds_duration: { + value: 1, + unit: "d", + }, + }, + code: { + code: "Q6H", + display: "Every 6 hours", + system: "http://terminology.hl7.org/CodeSystem/v3-GTSAbbreviation", + }, + }, + }, + Q8H: { + display: "Q8H (Every 8 hours)", + timing: { + repeat: { + frequency: 1, + period: 8, + period_unit: "h", + bounds_duration: { + value: 1, + unit: "d", + }, + }, + code: { + code: "Q8H", + display: "Every 8 hours", + system: "http://terminology.hl7.org/CodeSystem/v3-GTSAbbreviation", + }, + }, + }, + BED: { + display: "BED (0-0-1)", + timing: { + repeat: { + frequency: 1, + period: 1, + period_unit: "d", + bounds_duration: { + value: 1, + unit: "d", + }, + }, + code: { + code: "BED", + display: "Bedtime", + system: "http://terminology.hl7.org/CodeSystem/v3-GTSAbbreviation", + }, + }, + }, + WK: { + display: "WK (Weekly)", + timing: { + repeat: { + frequency: 1, + period: 1, + period_unit: "wk", + bounds_duration: { + value: 1, + unit: "wk", + }, + }, + code: { + code: "WK", + display: "Weekly", + system: "http://terminology.hl7.org/CodeSystem/v3-GTSAbbreviation", + }, + }, + }, + MO: { + display: "MO (Monthly)", + timing: { + repeat: { + frequency: 1, + period: 1, + period_unit: "mo", + bounds_duration: { + value: 1, + unit: "mo", + }, + }, + code: { + code: "MO", + display: "Monthly", + system: "http://terminology.hl7.org/CodeSystem/v3-GTSAbbreviation", + }, + }, + }, +} as const; + +/** + * Attempt to parse a medication string into a single MedicationRequest object. + * + * - Handles parentheses in the name (e.g., "Indinavir (as indinavir sulfate) ...") + * - Handles numeric doses for mg, g, mcg, unit/mL, etc. + * - Detects route: "oral", "injection", etc. + * - Detects form: "tablet", "capsule", "solution for injection", "granules sachet", etc. + * + * You can extend the dictionaries & regex to cover more cases (IV, subcutaneous, brand names, etc.). + */ +export function parseMedicationStringToRequest( + medication: Code, +): MedicationRequest { + const medicationRequest: MedicationRequest = { + do_not_perform: false, + dosage_instruction: [ + { + as_needed_boolean: false, + }, + ], + medication, + status: "active", + intent: "order", + category: "inpatient", + priority: "routine", + }; - created_by?: UserBareMinimum; - updated_by?: UserBareMinimum; + return medicationRequest; } diff --git a/src/types/emr/medicationStatement.ts b/src/types/emr/medicationStatement.ts index 6d73504cded..877901e03b8 100644 --- a/src/types/emr/medicationStatement.ts +++ b/src/types/emr/medicationStatement.ts @@ -1,9 +1,11 @@ +import { UserBareMinimum } from "@/components/Users/models"; + import { Period } from "@/types/questionnaire/base"; import { Code } from "@/types/questionnaire/code"; export enum MedicationStatementInformationSourceType { PATIENT = "patient", - USER = "user", + PRACTITIONER = "practitioner", RELATED_PERSON = "related_person", } @@ -37,3 +39,19 @@ export type MedicationStatement = { note?: string; }; + +export type MedicationStatementRead = { + id: string; + status: MedicationStatementStatus; + reason?: string; + medication: Code; + dosage_text?: string; + effective_period?: Period; + encounter: string; + information_source?: MedicationStatementInformationSourceType; + note?: string; + created_at: string; + modified_at: string; + created_by: UserBareMinimum; + updated_by: UserBareMinimum; +};