diff --git a/public/locale/en.json b/public/locale/en.json index a6b499278b3..c3dda5691a2 100644 --- a/public/locale/en.json +++ b/public/locale/en.json @@ -431,7 +431,6 @@ "auth_method_unsupported": "This authentication method is not supported, please try a different method", "authorize_shift_delete": "Authorize shift delete", "auto_generated_for_care": "Auto Generated for Care", - "autofilled_fields": "Autofilled Fields", "available_features": "Available Features", "available_in": "Available in", "available_time_slots": "Available Time Slots", @@ -625,9 +624,7 @@ "continue_watching": "Continue watching", "contribute_github": "Contribute on Github", "copied_to_clipboard": "Copied to clipboard", - "copilot_thinking": "Copilot is thinking...", "copy_phone_number": "Copy Phone Number", - "could_not_autofill": "We could not autofill any fields from what you said", "could_not_load_page": "We are facing some difficulties showing the Page you were looking for. Our Engineers have been notified and we'll make sure that this is resolved on the fly!", "countries_travelled": "Countries travelled", "covid_19_cat_gov": "Covid_19 Clinical Category as per Govt. of Kerala guideline (A/B/C)", @@ -1480,7 +1477,6 @@ "prn_prescriptions": "PRN Prescriptions", "procedure_suggestions": "Procedure Suggestions", "procedures_select_placeholder": "Select procedures to add details", - "process_transcript": "Process Again", "professional_info": "Professional Information", "professional_info_note": "View or update user's professional information", "professional_info_note_self": "View or update your professional information", @@ -1703,7 +1699,6 @@ "start_consultation": "Start Consultation", "start_datetime": "Start Date/Time", "start_dosage": "Start Dosage", - "start_review": "Start Review", "state": "State", "status": "Status", "stop": "Stop", @@ -1752,9 +1747,6 @@ "total_patients": "Total Patients", "total_staff": "Total Staff", "total_users": "Total Users", - "transcribe_again": "Transcribe Again", - "transcript_edit_info": "You can update this if we made an error", - "transcript_information": "This is what we heard", "transfer_allowed": "Transfer Allowed", "transfer_blocked": "Transfer Blocked", "transfer_in_progress": "TRANSFER IN PROGRESS", @@ -1903,7 +1895,6 @@ "vitals": "Vitals", "vitals_monitor": "Vitals Monitor", "vitals_present": "Vitals Monitor present", - "voice_autofill": "Voice Autofill", "volunteer_assigned": "Volunteer assigned successfully", "volunteer_contact": "Volunteer Contact", "volunteer_contact_detail": "Provide the name and contact details of a volunteer who can assist the patient in emergencies. This should be someone outside the family.", diff --git a/src/Routers/routes/ConsultationRoutes.tsx b/src/Routers/routes/ConsultationRoutes.tsx index a55b00b3513..de81fe0efe8 100644 --- a/src/Routers/routes/ConsultationRoutes.tsx +++ b/src/Routers/routes/ConsultationRoutes.tsx @@ -39,6 +39,7 @@ const consultationRoutes: AppRoutes = { facilityId={facilityId} encounterId={encounterId} questionnaireSlug="encounter" + subjectType="encounter" patientId={patientId} /> ), diff --git a/src/Utils/scribe.tsx b/src/Utils/scribe.tsx new file mode 100644 index 00000000000..34fda998a7c --- /dev/null +++ b/src/Utils/scribe.tsx @@ -0,0 +1,40 @@ +import { useCallback, useState } from "react"; + +import { useValueInjection } from "./useValueInjectionObserver"; + +export default function ScribeStructuredInput(props: { + name: string; + prompt: string; + example: unknown; + value: T; + onChange: (value: T | undefined) => void; + className?: string; + children: React.ReactNode; +}) { + const { name, prompt, example, value, onChange, children, className } = props; + const [element, setElement] = useState(null); + + const callbackRef = useCallback( + (node: HTMLElement | null) => setElement(node), + [], + ); + + useValueInjection({ + targetElement: element, + onChange, + }); + + return ( +
+ {children} +
+ ); +} diff --git a/src/Utils/useValueInjectionObserver.tsx b/src/Utils/useValueInjectionObserver.tsx index ee1530bd3a0..d7d42ecad9d 100644 --- a/src/Utils/useValueInjectionObserver.tsx +++ b/src/Utils/useValueInjectionObserver.tsx @@ -53,3 +53,22 @@ export function useValueInjectionObserver(options: { return value; } + +export function useValueInjection(options: { + targetElement: HTMLElement | null; + attribute?: string; + onChange: (value: T | undefined) => void; +}) { + const { targetElement, attribute = "data-scribe-value", onChange } = options; + + const domValue = useValueInjectionObserver({ + targetElement, + attribute, + }); + + useEffect(() => { + onChange(domValue); + }, [domValue, targetElement, attribute]); + + return null; +} diff --git a/src/components/Patient/EncounterQuestionnaire.tsx b/src/components/Patient/EncounterQuestionnaire.tsx index f31aeb0839a..265e59c2a55 100644 --- a/src/components/Patient/EncounterQuestionnaire.tsx +++ b/src/components/Patient/EncounterQuestionnaire.tsx @@ -8,6 +8,8 @@ import { QuestionnaireForm } from "@/components/Questionnaire/QuestionnaireForm" import useAppHistory from "@/hooks/useAppHistory"; +import { PLUGIN_Component } from "@/PluginEngine"; + interface Props { facilityId: string; patientId: string; @@ -25,31 +27,34 @@ export default function EncounterQuestionnaire({ }: Props) { const { goBack } = useAppHistory(); return ( - - - - { - if (encounterId) { - navigate( - `/facility/${facilityId}/encounter/${encounterId}/updates`, - ); - } else { - navigate(`/patient/${patientId}/updates`); - } - }} - onCancel={() => goBack()} - /> - - - + <> + + + + + { + if (encounterId) { + navigate( + `/facility/${facilityId}/encounter/${encounterId}/updates`, + ); + } else { + navigate(`/patient/${patientId}/updates`); + } + }} + onCancel={() => goBack()} + /> + + + + ); } diff --git a/src/components/Questionnaire/QuestionTypes/AllergyQuestion.tsx b/src/components/Questionnaire/QuestionTypes/AllergyQuestion.tsx index 4e802f407a1..bf9a4cca6b6 100644 --- a/src/components/Questionnaire/QuestionTypes/AllergyQuestion.tsx +++ b/src/components/Questionnaire/QuestionTypes/AllergyQuestion.tsx @@ -41,6 +41,7 @@ import { import ValueSetSelect from "@/components/Questionnaire/ValueSetSelect"; +import ScribeStructuredInput from "@/Utils/scribe"; import { AllergyIntolerance } from "@/types/emr/allergyIntolerance/allergyIntolerance"; import { Code } from "@/types/questionnaire/code"; import { QuestionnaireResponse } from "@/types/questionnaire/form"; @@ -124,52 +125,100 @@ export function AllergyQuestion({ {question.text} {question.required && *} - {allergies.length > 0 && ( -
-
- - - - - Substance - - Clinical -
- Status -
- - Critical - - - Status - - - Occurrence - - -
-
- - {allergies.map((allergy, index) => ( - handleUpdateAllergy(index, updates)} - onRemove={() => handleRemoveAllergy(index)} - /> - ))} - -
+ { + if (value) { + updateQuestionnaireResponseCB({ + ...questionnaireResponse, + values: [ + { + type: "allergy_intolerance", + value: value, + }, + ], + }); + } + }} + name="Allergies" + prompt={`An array of objects of the following type: { + code: { + code: string, + display: string, + system: "http://snomed.info/sct" + }, + clinical_status?: "active" | "inactive" | "resolved", + category?: "food" | "medication" | "environment" | "biologic", + criticality?: "low" | "high" | "unable-to-assess", + verification?: "unconfirmed" | "presumed" | "confirmed" | "refuted" | "entered-in-error" + last_occurrence?: YYYY-MM-DD string, + note?: string + }. Update existing data, delete existing data or append to the existing list as per the will of the user. Current date is ${new Date().toLocaleDateString()}`} + example={[ + { + code: { + code: "842825221000119100", + display: "Anifrolumab", + system: "http://snomed.info/sct", + }, + clinical_status: "inactive", + category: "environment", + criticality: "high", + last_occurrence: "2024-12-11", + note: "212", + }, + ]} + className="rounded-lg border p-4" + > + {allergies.length > 0 && ( +
+
+ + + + + Substance + + Clinical +
+ Status +
+ + Critical + + + Status + + + Occurrence + + +
+
+ + {allergies.map((allergy, index) => ( + + handleUpdateAllergy(index, updates) + } + onRemove={() => handleRemoveAllergy(index)} + /> + ))} + +
+
-
- )} - + )} + +
); } diff --git a/src/components/Questionnaire/QuestionTypes/ChoiceQuestion.tsx b/src/components/Questionnaire/QuestionTypes/ChoiceQuestion.tsx index cb14f07ad59..2943cad6354 100644 --- a/src/components/Questionnaire/QuestionTypes/ChoiceQuestion.tsx +++ b/src/components/Questionnaire/QuestionTypes/ChoiceQuestion.tsx @@ -1,17 +1,11 @@ import { memo } from "react"; import { Label } from "@/components/ui/label"; -import { - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue, -} from "@/components/ui/select"; +import Select from "@/components/ui/select-util"; import { properCase } from "@/Utils/utils"; import type { QuestionnaireResponse } from "@/types/questionnaire/form"; -import type { AnswerOption, Question } from "@/types/questionnaire/question"; +import type { Question } from "@/types/questionnaire/question"; interface ChoiceQuestionProps { question: Question; @@ -57,23 +51,13 @@ export const ChoiceQuestion = memo(function ChoiceQuestion({ + options={options.map((option) => ({ + label: properCase(option.display || option.value), + value: option.value.toString(), + }))} + /> ); }); diff --git a/src/components/Questionnaire/QuestionTypes/DateTimeQuestion.tsx b/src/components/Questionnaire/QuestionTypes/DateTimeQuestion.tsx index b83e593452c..b8587d2c9de 100644 --- a/src/components/Questionnaire/QuestionTypes/DateTimeQuestion.tsx +++ b/src/components/Questionnaire/QuestionTypes/DateTimeQuestion.tsx @@ -1,4 +1,5 @@ import { format } from "date-fns"; +import dayjs from "dayjs"; import { cn } from "@/lib/utils"; @@ -14,6 +15,7 @@ import { PopoverTrigger, } from "@/components/ui/popover"; +import ScribeStructuredInput from "@/Utils/scribe"; import type { QuestionnaireResponse } from "@/types/questionnaire/form"; import type { Question } from "@/types/questionnaire/question"; @@ -91,39 +93,53 @@ export function DateTimeQuestion({ {question.text} {question.required && *} -
- - - - - - - - - -
+ + value={{ date: currentValue?.toISOString() || "" }} + onChange={(value) => { + if (value?.date) { + const date = dayjs(value.date).toDate(); + console.log(value, date); + handleUpdate(date); + } + }} + name={question.text} + prompt={`A date in ISO format, minus 5 hour 30 minutes in the following format : {date: datestring}. Current time for your reference is ${new Date().toISOString()}`} + example={{ date: new Date().toISOString() }} + > +
+ + + + + + + + + +
+ ); } diff --git a/src/components/Questionnaire/QuestionTypes/DiagnosisQuestion.tsx b/src/components/Questionnaire/QuestionTypes/DiagnosisQuestion.tsx index c1ad524503b..ac520fe8049 100644 --- a/src/components/Questionnaire/QuestionTypes/DiagnosisQuestion.tsx +++ b/src/components/Questionnaire/QuestionTypes/DiagnosisQuestion.tsx @@ -25,6 +25,7 @@ import { import ValueSetSelect from "@/components/Questionnaire/ValueSetSelect"; +import ScribeStructuredInput from "@/Utils/scribe"; import { DIAGNOSIS_CLINICAL_STATUS, DIAGNOSIS_VERIFICATION_STATUS, @@ -95,34 +96,77 @@ export function DiagnosisQuestion({ {question.text} {question.required && *} - {diagnoses.length > 0 && ( -
-
-
Diagnosis
-
Onset Date
-
Status
-
Verification
-
Action
-
-
- {diagnoses.map((diagnosis, index) => ( - handleUpdateDiagnosis(index, updates)} - onRemove={() => handleRemoveDiagnosis(index)} - /> - ))} + { + if (value) { + updateQuestionnaireResponseCB({ + ...questionnaireResponse, + values: [ + { + type: "diagnosis", + value, + }, + ], + }); + } + }} + name="Diagnoses" + prompt={`An array of objects of the following type, based on the SNOMED CT Code for the applicable diagnoses: { + code: {"code" : string, "display" : string, "system" : "http://snomed.info/sct"}, + clinical_status: "active" | "recurrence" | "relapse" | "inactive" | "remission" | "resolved", + verification_status: "unconfirmed" | "provisional" | "differential" | "confirmed" | "refuted" | "entered-in-error", + onset: { + onset_datetime: YYYY-MM-DD string + }, + note?: string + }. Update existing data, delete existing data or append to the existing list as per the will of the user. Current date is ${new Date().toLocaleDateString()} Default onset_datetime to today unless otherwise specified`} + example={[ + { + code: { + code: "972900701000119109", + display: "Venous ulcer of toe of left foot", + system: "http://snomed.info/sct", + }, + clinical_status: "recurrence", + verification_status: "provisional", + onset: { + onset_datetime: "2024-12-03", + }, + note: "Note here", + }, + ]} + className="rounded-lg border p-4" + > + {diagnoses.length > 0 && ( +
+
+
Diagnosis
+
Onset Date
+
Status
+
Verification
+
Action
+
+
+ {diagnoses.map((diagnosis, index) => ( + handleUpdateDiagnosis(index, updates)} + onRemove={() => handleRemoveDiagnosis(index)} + /> + ))} +
-
- )} - + )} + +
); } diff --git a/src/components/Questionnaire/QuestionTypes/EncounterQuestion.tsx b/src/components/Questionnaire/QuestionTypes/EncounterQuestion.tsx index 00ba6ce0536..7bfc2aba6ca 100644 --- a/src/components/Questionnaire/QuestionTypes/EncounterQuestion.tsx +++ b/src/components/Questionnaire/QuestionTypes/EncounterQuestion.tsx @@ -3,13 +3,7 @@ import { useEffect, useState } from "react"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; -import { - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue, -} from "@/components/ui/select"; +import Select from "@/components/ui/select-util"; import routes from "@/Utils/request/api"; import query from "@/Utils/request/query"; @@ -152,87 +146,76 @@ export function EncounterQuestion({ + options={[ + { value: "planned", label: "Planned" }, + { value: "in_progress", label: "In Progress" }, + { value: "on_hold", label: "On Hold" }, + { value: "discharged", label: "Discharged" }, + { value: "completed", label: "Completed" }, + { value: "cancelled", label: "Cancelled" }, + { value: "discontinued", label: "Discontinued" }, + { value: "entered_in_error", label: "Entered in Error" }, + { value: "unknown", label: "Unknown" }, + ]} + />
+ options={[ + { value: "imp", label: "Inpatient (IP)" }, + { value: "amb", label: "Ambulatory (OP)" }, + { value: "obsenc", label: "Observation Room" }, + { value: "emer", label: "Emergency" }, + { value: "vr", label: "Virtual" }, + { value: "hh", label: "Home Health" }, + ]} + />
+ options={[ + { value: "ASAP", label: "ASAP" }, + { value: "callback_results", label: "Callback Results" }, + { + value: "callback_for_scheduling", + label: "Callback for Scheduling", + }, + { value: "elective", label: "Elective" }, + { value: "emergency", label: "Emergency" }, + { value: "preop", label: "Pre-op" }, + { value: "as_needed", label: "As Needed" }, + { value: "routine", label: "Routine" }, + { value: "rush_reporting", label: "Rush Reporting" }, + { value: "stat", label: "Stat" }, + { value: "timing_critical", label: "Timing Critical" }, + { value: "use_as_directed", label: "Use as Directed" }, + { value: "urgent", label: "Urgent" }, + ]} + />
@@ -283,8 +266,8 @@ export function EncounterQuestion({
+ options={[ + { value: "hosp_trans", label: "Hospital Transfer" }, + { value: "emd", label: "Emergency Department" }, + { value: "outp", label: "Outpatient Department" }, + { value: "born", label: "Born" }, + { value: "gp", label: "General Practitioner" }, + { value: "mp", label: "Medical Practitioner" }, + { value: "nursing", label: "Nursing Home" }, + { value: "psych", label: "Psychiatric Hospital" }, + { value: "rehab", label: "Rehabilitation Facility" }, + { value: "other", label: "Other" }, + ]} + placeholder="Select admit source" + />
{/* Show discharge disposition only when status is completed */} @@ -324,8 +304,10 @@ export function EncounterQuestion({
+ options={[ + { value: "home", label: "Home" }, + { value: "alt_home", label: "Alternate Home" }, + { value: "other_hcf", label: "Other Healthcare Facility" }, + { value: "hosp", label: "Hospice" }, + { value: "long", label: "Long Term Care" }, + { value: "aadvice", label: "Left Against Advice" }, + { value: "exp", label: "Expired" }, + { value: "psy", label: "Psychiatric Hospital" }, + { value: "rehab", label: "Rehabilitation" }, + { value: "snf", label: "Skilled Nursing Facility" }, + { value: "oth", label: "Other" }, + ]} + placeholder="Select discharge disposition" + />
)}
+ options={[ + { value: "vegetarian", label: "Vegetarian" }, + { value: "diary_free", label: "Dairy Free" }, + { value: "nut_free", label: "Nut Free" }, + { value: "gluten_free", label: "Gluten Free" }, + { value: "vegan", label: "Vegan" }, + { value: "halal", label: "Halal" }, + { value: "kosher", label: "Kosher" }, + { value: "none", label: "None" }, + ]} + placeholder="Select diet preference" + />
diff --git a/src/components/Questionnaire/QuestionTypes/MedicationRequestQuestion.tsx b/src/components/Questionnaire/QuestionTypes/MedicationRequestQuestion.tsx index dd9cb4322aa..9314f4f4f37 100644 --- a/src/components/Questionnaire/QuestionTypes/MedicationRequestQuestion.tsx +++ b/src/components/Questionnaire/QuestionTypes/MedicationRequestQuestion.tsx @@ -16,6 +16,7 @@ import { import { QuantityInput } from "@/components/Common/QuantityInput"; import ValueSetSelect from "@/components/Questionnaire/ValueSetSelect"; +import ScribeStructuredInput from "@/Utils/scribe"; import { DOSAGE_UNITS, MEDICATION_REQUEST_INTENT, @@ -33,7 +34,6 @@ interface MedicationRequestQuestionProps { updateQuestionnaireResponseCB: (response: QuestionnaireResponse) => void; disabled?: boolean; } - const MEDICATION_REQUEST_INITIAL_VALUE: MedicationRequest = { status: "active", intent: "order", @@ -44,7 +44,6 @@ const MEDICATION_REQUEST_INITIAL_VALUE: MedicationRequest = { authored_on: new Date().toISOString(), dosage_instruction: [], }; - export function MedicationRequestQuestion({ question, questionnaireResponse, @@ -53,7 +52,6 @@ export function MedicationRequestQuestion({ }: MedicationRequestQuestionProps) { const medications = (questionnaireResponse.values?.[0]?.value as MedicationRequest[]) || []; - const handleAddMedication = (medication: Code) => { const newMedications: MedicationRequest[] = [ ...medications, @@ -73,7 +71,6 @@ export function MedicationRequestQuestion({ ], }); }; - const handleRemoveMedication = (index: number) => { const newMedications = medications.filter((_, i) => i !== index); updateQuestionnaireResponseCB({ @@ -81,7 +78,6 @@ export function MedicationRequestQuestion({ values: [{ type: "medication_request", value: newMedications }], }); }; - const handleUpdateMedication = ( index: number, updates: Partial, @@ -89,7 +85,6 @@ export function MedicationRequestQuestion({ const newMedications = medications.map((medication, i) => i === index ? { ...medication, ...updates } : medication, ); - updateQuestionnaireResponseCB({ ...questionnaireResponse, values: [ @@ -100,15 +95,173 @@ export function MedicationRequestQuestion({ ], }); }; - return (
- {medications.length > 0 && ( -
+ { + if (value) { + updateQuestionnaireResponseCB({ + ...questionnaireResponse, + values: [ + { + type: "medication_request", + value, + }, + ], + }); + } + }} + name="Medication Request" + prompt={`An array of objects of the following type, based on a SNOWMED Medication Product like "Paracetamol containing product": { + status?: "active" | "on-hold" | "ended" | "stopped" | "completed" | "cancelled" | "entered-in-error" | "draft" | "unknown", + intent?: "proposal" | "plan" | "order" | "original_order" | "reflex_order" | "filler_order" | "instance_order", + category?: "inpatient" | "outpatient" | "community" | "discharge", + priority?: "stat" | "urgent" | "asap" | "routine" + do_not_perform?: boolean; + medication? : CodeType; + authored_on?: ISO time string, + dosage_instruction: { + sequence?: number; + text?: string; + additional_instruction?: { + system: string; + code: string; + display?: string; + }[]; + patient_instruction?: string; + timing?: { + repeat?: { + frequency?: number; + period: number; // number of units (ex. 12 days would mean 12 with unit "d") + period_unit: "s" | "min" | "h" | "d" | "wk" | "mo" | "a"; + }; + }; + /** + * True if it is a PRN medication + */ + as_needed_boolean?: boolean; + /** + * If it is a PRN medication (as_needed_boolean is true), the indicator. + */ + as_needed_for?: CodeType; + site?: CodeType; + route?: CodeType; + method?: CodeType; + /** + * 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. + */ + dose_and_rate?: ( + | { + type?: "ordered"; + dose_quantity?: DosageQuantity; + dose_range?: undefined; + } + | { + type: "calculated"; + dose_range?: { + low: DosageQuantity; + high: DosageQuantity; + }; + dose_quantity?: undefined; + } + )[]; + max_dose_per_period?: { + low: DosageQuantity; + high: DosageQuantity; + }; + }[]; + note?: string + }. + + DosageQuantity { + value?: number; + unit?: "mg" | "g" | "ml" | "drop(s)" | "ampule(s)" | "tsp" | "mcg" | "unit(s)" + } + + CodeType { + code: string, + display: string, + system: "http://snomed.info/sct" + } + + Update existing data, delete existing data or append to the existing list as per the will of the user. Current date is ${new Date().toLocaleDateString()}`} + example={[ + { + status: "active", + intent: "original_order", + category: "inpatient", + priority: "urgent", + do_not_perform: false, + medication: { + code: "1214771000202109", + display: + "Ciprofloxacin and fluocinolone only product in otic dose form", + system: "http://snomed.info/sct", + }, + authored_on: "2024-12-29T22:16:45.404Z", + dosage_instruction: [ + { + dose_and_rate: [ + { + type: "ordered", + dose_quantity: { + unit: "g", + value: 11, + }, + }, + ], + route: { + code: "58831000052108", + display: "Subretinal route", + system: "http://snomed.info/sct", + }, + method: { + code: "1231460007", + display: "Dialysis system", + system: "http://snomed.info/sct", + }, + site: { + code: "16217661000119109", + display: "Structure of right deltoid muscle", + system: "http://snomed.info/sct", + }, + as_needed_boolean: true, + timing: { + repeat: { + frequency: 1, + period: 1, + period_unit: "d", + }, + }, + additional_instruction: [ + { + code: "421984009", + display: "Until finished", + system: "http://snomed.info/sct", + }, + ], + as_needed_for: { + code: "972604701000119104", + display: + "Acquired arteriovenous malformation of vascular structure of gastrointestinal tract", + system: "http://snomed.info/sct", + }, + }, + ], + }, + ]} + className="rounded-lg border space-y-4" + > + {medications.length > 0 && (
    {medications.map((medication, index) => (
  • @@ -124,8 +277,8 @@ export function MedicationRequestQuestion({
  • ))}
-
- )} + )} + ); } - export const MedicationRequestItem: React.FC<{ medication: MedicationRequest; disabled?: boolean; @@ -152,7 +304,6 @@ export const MedicationRequestItem: React.FC<{ dosage_instruction: [{ ...dosageInstruction, ...updates }], }); }; - return (
@@ -238,7 +389,6 @@ export const MedicationRequestItem: React.FC<{ />
-
@@ -251,7 +401,6 @@ export const MedicationRequestItem: React.FC<{ />
-
- {medication.dosage_instruction[0]?.as_needed_boolean ? (
@@ -320,7 +468,6 @@ export const MedicationRequestItem: React.FC<{
)} -
- {/*
{JSON.stringify(medication, null, 2)}
*/} ); }; - const reverseFrequencyOption = ( option: MedicationRequest["dosage_instruction"][0]["timing"], ) => { @@ -364,7 +509,6 @@ const reverseFrequencyOption = ( value.timing.repeat.period === option?.repeat?.period, )?.[0] as keyof typeof FREQUENCY_OPTIONS; }; - // TODO: verify period_unit is correct const FREQUENCY_OPTIONS = { BD: { diff --git a/src/components/Questionnaire/QuestionTypes/MedicationStatementQuestion.tsx b/src/components/Questionnaire/QuestionTypes/MedicationStatementQuestion.tsx index 7c237b35f5d..6ee32bcd9c8 100644 --- a/src/components/Questionnaire/QuestionTypes/MedicationStatementQuestion.tsx +++ b/src/components/Questionnaire/QuestionTypes/MedicationStatementQuestion.tsx @@ -3,7 +3,7 @@ import { QuestionMarkCircledIcon, TextAlignLeftIcon, } from "@radix-ui/react-icons"; -import React from "react"; +import React, { useState } from "react"; import { useTranslation } from "react-i18next"; import CareIcon, { IconName } from "@/CAREUI/icons/CareIcon"; @@ -23,6 +23,7 @@ import { Textarea } from "@/components/ui/textarea"; import ValueSetSelect from "@/components/Questionnaire/ValueSetSelect"; +import ScribeStructuredInput from "@/Utils/scribe"; import { MEDICATION_STATEMENT_STATUS, MedicationStatement, @@ -60,8 +61,11 @@ export function MedicationStatementQuestion({ }: MedicationStatementQuestionProps) { const { t } = useTranslation(); - const medications = - (questionnaireResponse.values?.[0]?.value as MedicationStatement[]) || []; + const [medications, setMedications] = useState(() => { + return ( + (questionnaireResponse.values?.[0]?.value as MedicationStatement[]) || [] + ); + }); const handleAddMedication = (medication: Code) => { const newMedications: Omit< @@ -115,25 +119,80 @@ export function MedicationStatementQuestion({ {question.text} {question.required && *} - {medications.length > 0 && ( -
-
    - {medications.map((medication, index) => ( -
  • - - handleUpdateMedication(index, medication) - } - onRemove={() => handleRemoveMedication(index)} - index={index} - /> -
  • - ))} -
-
- )} + { + if (value) { + setMedications(value); + updateQuestionnaireResponseCB({ + ...questionnaireResponse, + values: [ + { + type: "medication_statement", + value: value, + }, + ], + }); + } + }} + name="Medication Statement" + prompt={`An array of objects of the following type: { + status?: ${MEDICATION_STATEMENT_STATUS.join(" | ")}, + dosage_text?: string, + information_source?: "patient" | "user" | "related_person" + medication?: { + code: string, + display: string, + system: "http://snomed.info/sct" + }, + note?: string, + reason?: string, + effective_period?: { + start: ISO date string, + end: ISO date string + } + }. Update existing data, delete existing data or append to the existing list as per the will of the user. Current date is ${new Date().toLocaleDateString()}`} + example={[ + { + status: "completed", + dosage_text: "10 ml", + information_source: "patient", + medication: { + code: "1213681000202103", + display: "Cabotegravir only product in oral dose form", + system: "http://snomed.info/sct", + }, + note: "a note", + reason: "patient was feeling dizzy", + effective_period: { + start: "2024-12-12T18:30:00.000Z", + end: "2025-01-07T18:30:00.000Z", + }, + }, + ]} + className="rounded-lg border p-4" + > + {medications.length > 0 && ( +
+
    + {medications.map((medication, index) => ( +
  • + + handleUpdateMedication(index, medication) + } + onRemove={() => handleRemoveMedication(index)} + index={index} + /> +
  • + ))} +
+
+ )} +
+ *} - {symptoms.length > 0 && ( -
-
-
Symptom
-
Date
-
Status
-
Severity
-
Action
-
-
- {symptoms.map((symptom, index) => ( - handleUpdateSymptom(index, updates)} - onRemove={() => handleRemoveSymptom(index)} - /> - ))} + { + if (value) { + updateQuestionnaireResponseCB({ + ...questionnaireResponse, + values: [ + { + type: "symptom", + value, + }, + ], + }); + } + }} + name="Symptoms" + prompt={`An array of objects of the following type, based on the SNOMED CT Code for the applicable symptoms: { + code: {"code" : string, "display" : string, "system" : "http://snomed.info/sct"}, + clinical_status: "active" | "recurrence" | "relapse" | "inactive" | "remission" | "resolved", + verification_status: "unconfirmed" | "provisional" | "differential" | "confirmed" | "refuted" | "entered-in-error", + severity?: "severe" | "moderate" | "mild", + onset?: { + onset_datetime: YYYY-MM-DD string + }, + note?: string + }. Update existing data, delete existing data or append to the existing list as per the will of the user. Current date is ${new Date().toLocaleDateString()} Default onset_datetime to today unless otherwise specified`} + example={[ + { + code: { + code: "972900701000119109", + display: "Venous ulcer of toe of left foot", + system: "http://snomed.info/sct", + }, + clinical_status: "recurrence", + verification_status: "provisional", + severity: "severe", + onset: { + onset_datetime: "2024-12-03", + }, + note: "Note here", + }, + ]} + className="rounded-lg border p-4" + > + {symptoms.length > 0 && ( +
+
+
Symptom
+
Date
+
Status
+
Severity
+
Action
+
+
+ {symptoms.map((symptom, index) => ( + handleUpdateSymptom(index, updates)} + onRemove={() => handleRemoveSymptom(index)} + /> + ))} +
-
- )} - + )} + +
); } diff --git a/src/components/Questionnaire/QuestionnaireForm.tsx b/src/components/Questionnaire/QuestionnaireForm.tsx index ad9e802ade0..b3fffa9fa42 100644 --- a/src/components/Questionnaire/QuestionnaireForm.tsx +++ b/src/components/Questionnaire/QuestionnaireForm.tsx @@ -252,7 +252,7 @@ export function QuestionnaireForm({ }; return ( -
+
{/* Left Navigation */}
{questionnaireForms.map((form) => ( @@ -427,14 +427,14 @@ export function QuestionnaireForm({ )} {/* Add a Preview of the QuestionnaireForm */} - {import.meta.env.DEV && ( + {/* {import.meta.env.DEV && (

QuestionnaireForm

               {JSON.stringify(questionnaireForms, null, 2)}
             
- )} + )} */}
); diff --git a/src/components/Schedule/Appointments/AppointmentDetailsPage.tsx b/src/components/Schedule/Appointments/AppointmentDetailsPage.tsx index 55bba4d2862..035b99a400a 100644 --- a/src/components/Schedule/Appointments/AppointmentDetailsPage.tsx +++ b/src/components/Schedule/Appointments/AppointmentDetailsPage.tsx @@ -372,7 +372,14 @@ const AppointmentActions = ({ const isToday = isSameDay(appointment.token_slot.start_datetime, new Date()); if (["fulfilled", "cancelled", "entered_in_error"].includes(currentStatus)) { - return null; + return ( +
+ +
+ ); } if (!["booked", "checked_in", "in_consultation"].includes(currentStatus)) { diff --git a/src/components/ui/select-util.tsx b/src/components/ui/select-util.tsx new file mode 100644 index 00000000000..d05ce9ff3a6 --- /dev/null +++ b/src/components/ui/select-util.tsx @@ -0,0 +1,63 @@ +import { useCallback, useState } from "react"; + +import { useValueInjection } from "@/Utils/useValueInjectionObserver"; + +import { + SelectContent, + SelectItem, + Select as SelectStandalone, + SelectTrigger, + SelectValue, +} from "./select"; + +export default function Select(props: { + value?: string; + onChange: (value: string) => void; + placeholder?: string; + options: { + label: React.ReactNode; + value: string; + }[]; + disabled?: boolean; +}) { + const [element, setElement] = useState(null); + const callbackRef = useCallback( + (node: HTMLElement | null) => setElement(node), + [], + ); + + useValueInjection({ + targetElement: element, + attribute: "data-cui-listbox-value", + onChange: (value) => value && props.onChange(value), + }); + + return ( + + [ + option.value, + option.label?.toString(), + ]), + )} + data-cui-listbox-value={JSON.stringify(props.value)} + > + + + + {props.options.map((option) => ( + + {option.label} + + ))} + + + ); +} diff --git a/src/components/ui/switch.tsx b/src/components/ui/switch.tsx index d125146c189..6125e4bb1c9 100644 --- a/src/components/ui/switch.tsx +++ b/src/components/ui/switch.tsx @@ -3,25 +3,37 @@ import * as React from "react"; import { cn } from "@/lib/utils"; +import { useValueInjection } from "@/Utils/useValueInjectionObserver"; + const Switch = React.forwardRef< React.ElementRef, React.ComponentPropsWithoutRef ->(({ className, ...props }, ref) => ( - - (({ className, ...props }, ref) => { + useValueInjection({ + targetElement: ref as any, + attribute: "data-cui-checked", + onChange: (value) => value && props.onCheckedChange?.(value === "true"), + }); + + return ( + - -)); + {...props} + data-cui-checkbox + data-cui-checked={props.checked} + ref={ref} + > + + + ); +}); Switch.displayName = SwitchPrimitives.Root.displayName; export { Switch };