From bd8ac7e93dbe49cd6b0b0c73a157e363892fdf5f Mon Sep 17 00:00:00 2001 From: Amjith Titus Date: Tue, 7 Jan 2025 18:38:42 +0530 Subject: [PATCH] Allergy intolerance Cleanup (#9812) --- .../QuestionnaireResponsesList.tsx | 110 ++---- .../StructuredResponseView.tsx | 99 ----- src/components/Patient/allergy/list.tsx | 6 +- .../Questionnaire/QuestionRenderer.tsx | 3 + .../QuestionTypes/AllergyQuestion.tsx | 348 ++++++++++++++++-- .../QuestionTypes/QuestionGroup.tsx | 4 + .../QuestionTypes/QuestionInput.tsx | 3 + .../Questionnaire/QuestionnaireForm.tsx | 1 + .../Questionnaire/structured/handlers.ts | 32 +- .../Questionnaire/structured/types.ts | 9 +- .../Encounters/tabs/EncounterUpdatesTab.tsx | 6 + .../allergyIntolerance/allergyIntolerance.ts | 11 +- src/types/questionnaire/form.ts | 4 +- 13 files changed, 397 insertions(+), 239 deletions(-) delete mode 100644 src/components/Facility/ConsultationDetails/StructuredResponseView.tsx diff --git a/src/components/Facility/ConsultationDetails/QuestionnaireResponsesList.tsx b/src/components/Facility/ConsultationDetails/QuestionnaireResponsesList.tsx index 11f9ea4529b..3cbad3a75c6 100644 --- a/src/components/Facility/ConsultationDetails/QuestionnaireResponsesList.tsx +++ b/src/components/Facility/ConsultationDetails/QuestionnaireResponsesList.tsx @@ -1,10 +1,8 @@ -import { useState } from "react"; import { useTranslation } from "react-i18next"; import CareIcon from "@/CAREUI/icons/CareIcon"; import PaginatedList from "@/CAREUI/misc/PaginatedList"; -import { Button } from "@/components/ui/button"; import { Card } from "@/components/ui/card"; import routes from "@/Utils/request/api"; @@ -13,8 +11,6 @@ import { Encounter } from "@/types/emr/encounter"; import { Question } from "@/types/questionnaire/question"; import { QuestionnaireResponse } from "@/types/questionnaire/questionnaireResponse"; -import { StructuredResponseView } from "./StructuredResponseView"; - interface Props { encounter: Encounter; } @@ -128,21 +124,6 @@ function QuestionGroup({ export default function QuestionnaireResponsesList({ encounter }: Props) { const { t } = useTranslation(); - const [expandedResponseIds, setExpandedResponseIds] = useState>( - new Set(), - ); - - const toggleResponse = (id: string) => { - setExpandedResponseIds((prev) => { - const next = new Set(prev); - if (next.has(id)) { - next.delete(id); - } else { - next.add(id); - } - return next; - }); - }; return ( {formatDateTime(item.created_date)} - by {item.created_by?.first_name || ""}{" "} - {item.created_by?.last_name || ""} - {` (${item.created_by?.user_type})`} + {!item.questionnaire && ( + <> + {Object.values( + item.structured_responses ?? {}, + )[0]?.submit_type === "CREATE" + ? "Created" + : "Updated"}{" "} + + )} + { + <> + by {item.created_by?.first_name || ""}{" "} + {item.created_by?.last_name || ""} + {item.created_by?.user_type && + ` (${item.created_by?.user_type})`} + + } - - {expandedResponseIds.has(item.id) && ( + {item.questionnaire && (
- {item.questionnaire ? ( - // Existing questionnaire response rendering -
- {item.questionnaire?.questions.map( - (question: Question) => { - // Skip structured questions for now as they need special handling - if (question.type === "structured") return null; - - const response = item.responses.find( - (r) => r.question_id === question.id, - ); +
+ {item.questionnaire?.questions.map( + (question: Question) => { + // Skip structured questions for now as they need special handling + if (question.type === "structured") return null; - if (question.type === "group") { - return ( - - ); - } - - if (!response) return null; + const response = item.responses.find( + (r) => r.question_id === question.id, + ); + if (question.type === "group") { return ( - ); - }, - )} -
- ) : item.structured_responses ? ( - // New structured response rendering - Object.entries(item.structured_responses).map( - ([type, response]) => { + } + + if (!response) return null; + return ( - ); }, - ) - ) : null} + )} +
)} diff --git a/src/components/Facility/ConsultationDetails/StructuredResponseView.tsx b/src/components/Facility/ConsultationDetails/StructuredResponseView.tsx deleted file mode 100644 index 9d6b06ae2b8..00000000000 --- a/src/components/Facility/ConsultationDetails/StructuredResponseView.tsx +++ /dev/null @@ -1,99 +0,0 @@ -import { useQuery } from "@tanstack/react-query"; - -import { AllergyTable } from "@/components/Patient/allergy/AllergyTable"; -import { DiagnosisTable } from "@/components/Patient/diagnosis/DiagnosisTable"; -import { SymptomTable } from "@/components/Patient/symptoms/SymptomTable"; - -import query from "@/Utils/request/query"; -import { AllergyIntolerance } from "@/types/emr/allergyIntolerance/allergyIntolerance"; -import allergyApi from "@/types/emr/allergyIntolerance/allergyIntoleranceApi"; -import { Diagnosis } from "@/types/emr/diagnosis/diagnosis"; -import diagnosisApi from "@/types/emr/diagnosis/diagnosisApi"; -import { Symptom } from "@/types/emr/symptom/symptom"; -import symptomApi from "@/types/emr/symptom/symptomApi"; - -interface Props { - type: string; - id: string; - patientId: string; - encounterId: string; -} - -export function StructuredResponseView({ - type, - id, - patientId, - encounterId, -}: Props) { - const getRouteAndParams = () => { - const params: Record = { patientId }; - switch (type) { - case "symptom": - return { - route: symptomApi.retrieveSymptom, - pathParams: { ...params, symptomId: id }, - queryParams: { encounter: encounterId }, - }; - case "diagnosis": - return { - route: diagnosisApi.retrieveDiagnosis, - pathParams: { ...params, diagnosisId: id }, - queryParams: { encounter: encounterId }, - }; - case "allergy_intolerance": - return { - route: allergyApi.retrieveAllergy, - pathParams: { ...params, allergyId: id }, - queryParams: { encounter: encounterId }, - }; - } - }; - - const routeConfig = getRouteAndParams(); - - const { data, isLoading, error } = useQuery({ - queryKey: [type, id], - queryFn: query(routeConfig?.route as any, { - pathParams: routeConfig?.pathParams, - queryParams: routeConfig?.queryParams, - }), - enabled: !!id && !!routeConfig, - }); - - if (!routeConfig) return null; - - if (isLoading) { - return
; - } - - if (error) { - console.error(`Error loading ${type}:`, error); - return
Error loading {type}
; - } - - switch (type) { - case "symptom": - return ( - - ); - case "diagnosis": - return ( - - ); - case "allergy_intolerance": - return ( - - ); - default: - return null; - } -} diff --git a/src/components/Patient/allergy/list.tsx b/src/components/Patient/allergy/list.tsx index 372b353563e..284b7885a70 100644 --- a/src/components/Patient/allergy/list.tsx +++ b/src/components/Patient/allergy/list.tsx @@ -20,13 +20,15 @@ import allergyIntoleranceApi from "@/types/emr/allergyIntolerance/allergyIntoler interface AllergyListProps { patientId: string; + encounterId?: string; } -export function AllergyList({ patientId }: AllergyListProps) { +export function AllergyList({ patientId, encounterId }: AllergyListProps) { const { data: allergies, isLoading } = useQuery({ - queryKey: ["allergies", patientId], + queryKey: ["allergies", patientId, encounterId], queryFn: query(allergyIntoleranceApi.getAllergy, { pathParams: { patientId }, + queryParams: encounterId ? { encounter: encounterId } : undefined, }), }); diff --git a/src/components/Questionnaire/QuestionRenderer.tsx b/src/components/Questionnaire/QuestionRenderer.tsx index 8882f38b162..71b04cb0a77 100644 --- a/src/components/Questionnaire/QuestionRenderer.tsx +++ b/src/components/Questionnaire/QuestionRenderer.tsx @@ -16,6 +16,7 @@ interface QuestionRendererProps { activeGroupId?: string; encounterId?: string; facilityId: string; + patientId: string; } export function QuestionRenderer({ @@ -28,6 +29,7 @@ export function QuestionRenderer({ activeGroupId, encounterId, facilityId, + patientId, }: QuestionRendererProps) { const questionRefs = useRef<{ [key: string]: HTMLDivElement | null }>({}); @@ -70,6 +72,7 @@ export function QuestionRenderer({ clearError={clearError} disabled={disabled} activeGroupId={activeGroupId} + patientId={patientId} />
))} diff --git a/src/components/Questionnaire/QuestionTypes/AllergyQuestion.tsx b/src/components/Questionnaire/QuestionTypes/AllergyQuestion.tsx index f0c0c6a5581..798c6dee303 100644 --- a/src/components/Questionnaire/QuestionTypes/AllergyQuestion.tsx +++ b/src/components/Questionnaire/QuestionTypes/AllergyQuestion.tsx @@ -1,17 +1,21 @@ "use client"; import { + CheckCircledIcon, + CircleBackslashIcon, DotsVerticalIcon, MinusCircledIcon, Pencil2Icon, } from "@radix-ui/react-icons"; +import { useQuery } from "@tanstack/react-query"; import { BeakerIcon, CookingPotIcon, HeartPulseIcon, LeafIcon, } from "lucide-react"; -import React, { useState } from "react"; +import React, { useEffect, useState } from "react"; +import { toast } from "sonner"; import { Button } from "@/components/ui/button"; import { @@ -41,19 +45,25 @@ import { import ValueSetSelect from "@/components/Questionnaire/ValueSetSelect"; -import { AllergyIntolerance } from "@/types/emr/allergyIntolerance/allergyIntolerance"; +import query from "@/Utils/request/query"; +import { + AllergyIntolerance, + AllergyIntoleranceRequest, +} from "@/types/emr/allergyIntolerance/allergyIntolerance"; +import allergyIntoleranceApi from "@/types/emr/allergyIntolerance/allergyIntoleranceApi"; import { Code } from "@/types/questionnaire/code"; import { QuestionnaireResponse } from "@/types/questionnaire/form"; import { Question } from "@/types/questionnaire/question"; interface AllergyQuestionProps { + patientId: string; question: Question; questionnaireResponse: QuestionnaireResponse; updateQuestionnaireResponseCB: (response: QuestionnaireResponse) => void; disabled?: boolean; } -const ALLERGY_INITIAL_VALUE: Partial = { +const ALLERGY_INITIAL_VALUE: Partial = { code: { code: "", display: "", system: "" }, clinical_status: "active", verification_status: "confirmed", @@ -77,19 +87,68 @@ const CATEGORY_ICONS: Record = { biologic: , }; +function convertToAllergyRequest( + allergy: AllergyIntolerance, +): AllergyIntoleranceRequest { + return { + id: allergy.id, + code: allergy.code, + clinical_status: allergy.clinical_status, + verification_status: allergy.verification_status, + category: allergy.category, + criticality: allergy.criticality, + last_occurrence: allergy.last_occurrence, + note: allergy.note, + encounter: allergy.encounter, + }; +} + export function AllergyQuestion({ questionnaireResponse, updateQuestionnaireResponseCB, disabled, + patientId, }: AllergyQuestionProps) { const allergies = - (questionnaireResponse.values?.[0]?.value as AllergyIntolerance[]) || []; + (questionnaireResponse.values?.[0]?.value as AllergyIntoleranceRequest[]) || + []; + + const { data: patientAllergies } = useQuery({ + queryKey: ["allergies", patientId], + queryFn: query(allergyIntoleranceApi.getAllergy, { + pathParams: { patientId }, + }), + }); + + useEffect(() => { + if (patientAllergies?.results && !allergies.length) { + updateQuestionnaireResponseCB({ + ...questionnaireResponse, + values: [ + { + type: "allergy_intolerance", + value: patientAllergies.results.map(convertToAllergyRequest), + }, + ], + }); + if (patientAllergies.count > patientAllergies.results.length) { + toast.info( + `Showing first ${patientAllergies.results.length} of ${patientAllergies.count} allergies`, + ); + } + } + }, [ + patientAllergies, + allergies.length, + questionnaireResponse, + updateQuestionnaireResponseCB, + ]); const handleAddAllergy = (code: Code) => { const newAllergies = [ ...allergies, { ...ALLERGY_INITIAL_VALUE, code }, - ] as AllergyIntolerance[]; + ] as AllergyIntoleranceRequest[]; updateQuestionnaireResponseCB({ ...questionnaireResponse, values: [{ type: "allergy_intolerance", value: newAllergies }], @@ -106,7 +165,7 @@ export function AllergyQuestion({ const handleUpdateAllergy = ( index: number, - updates: Partial, + updates: Partial, ) => { const newAllergies = allergies.map((allergy, i) => i === index ? { ...allergy, ...updates } : allergy, @@ -121,24 +180,19 @@ export function AllergyQuestion({ <> {allergies.length > 0 && (
-
+
Substance - - Clinical -
- Status -
Critical Status - + Occurrence @@ -157,6 +211,200 @@ export function AllergyQuestion({
+ +
+ {allergies.map((allergy, index) => ( +
+
+
+ + {allergy.code.display} +
+ + + + + + + handleUpdateAllergy(index, { + note: allergy.note !== undefined ? undefined : "", + }) + } + > + + {allergy.note !== undefined + ? "Hide Notes" + : "Add Notes"} + + {allergy.clinical_status !== "active" && ( + + handleUpdateAllergy(index, { + clinical_status: "active", + }) + } + > + + Mark Active + + )} + {allergy.clinical_status !== "inactive" && ( + + handleUpdateAllergy(index, { + clinical_status: "inactive", + }) + } + > + + Mark Inactive + + )} + {allergy.clinical_status !== "resolved" && ( + + handleUpdateAllergy(index, { + clinical_status: "resolved", + }) + } + > + + Mark Resolved + + )} + + handleRemoveAllergy(index)} + > + + Remove Allergy + + + +
+ +
+
+ + +
+ +
+ + +
+ +
+ + + handleUpdateAllergy(index, { + last_occurrence: e.target.value, + }) + } + disabled={disabled} + className="h-8 mt-1" + /> +
+
+ + {allergy.note !== undefined && ( +
+ + + handleUpdateAllergy(index, { note: e.target.value }) + } + disabled={disabled} + className="mt-1" + /> +
+ )} +
+ ))} +
)} ) => void; + onUpdate?: (allergy: Partial) => void; onRemove?: () => void; } const AllergyTableRow = ({ @@ -180,11 +428,29 @@ const AllergyTableRow = ({ onUpdate, onRemove, }: AllergyItemProps) => { - const [showNotes, setShowNotes] = useState(false); + const [showNotes, setShowNotes] = useState(allergy.note !== undefined); + + const rowClassName = `group ${ + allergy.clinical_status === "inactive" + ? "opacity-60" + : allergy.clinical_status === "resolved" + ? "line-through" + : "" + }`; + + const handleNotesToggle = () => { + if (showNotes) { + setShowNotes(false); + onUpdate?.({ note: undefined }); + } else { + setShowNotes(true); + onUpdate?.({ note: "" }); + } + }; return ( <> - + onUpdate?.({ clinical_status: value })} - disabled={disabled} - > - - - - - Active - Inactive - Resolved - - - - + - setShowNotes(!showNotes)}> + {showNotes ? "Hide Notes" : "Add Notes"} + {allergy.clinical_status !== "active" && ( + onUpdate?.({ clinical_status: "active" })} + > + + Mark Active + + )} + {allergy.clinical_status !== "inactive" && ( + onUpdate?.({ clinical_status: "inactive" })} + > + + Mark Inactive + + )} + {allergy.clinical_status !== "resolved" && ( + onUpdate?.({ clinical_status: "resolved" })} + > + + Mark Resolved + + )} {showNotes && ( - + onUpdate?.({ note: e.target.value })} disabled={disabled} className="mt-0.5" diff --git a/src/components/Questionnaire/QuestionTypes/QuestionGroup.tsx b/src/components/Questionnaire/QuestionTypes/QuestionGroup.tsx index 132f9628a70..f615964eeec 100644 --- a/src/components/Questionnaire/QuestionTypes/QuestionGroup.tsx +++ b/src/components/Questionnaire/QuestionTypes/QuestionGroup.tsx @@ -20,6 +20,7 @@ interface QuestionGroupProps { disabled?: boolean; activeGroupId?: string; facilityId: string; + patientId: string; } function isQuestionEnabled( @@ -83,6 +84,7 @@ export const QuestionGroup = memo(function QuestionGroup({ disabled, activeGroupId, facilityId, + patientId, }: QuestionGroupProps) { const isEnabled = isQuestionEnabled(question, questionnaireResponses); @@ -101,6 +103,7 @@ export const QuestionGroup = memo(function QuestionGroup({ clearError={() => clearError(question.id)} disabled={disabled} facilityId={facilityId} + patientId={patientId} /> ); } @@ -145,6 +148,7 @@ export const QuestionGroup = memo(function QuestionGroup({ clearError={clearError} disabled={disabled} activeGroupId={activeGroupId} + patientId={patientId} /> ))}
diff --git a/src/components/Questionnaire/QuestionTypes/QuestionInput.tsx b/src/components/Questionnaire/QuestionTypes/QuestionInput.tsx index c7df9d23153..e6249bbe491 100644 --- a/src/components/Questionnaire/QuestionTypes/QuestionInput.tsx +++ b/src/components/Questionnaire/QuestionTypes/QuestionInput.tsx @@ -36,6 +36,7 @@ interface QuestionInputProps { clearError: () => void; disabled?: boolean; facilityId: string; + patientId: string; } export function QuestionInput({ @@ -47,6 +48,7 @@ export function QuestionInput({ clearError, disabled, facilityId, + patientId, }: QuestionInputProps) { const questionnaireResponse = questionnaireResponses.find( (v) => v.question_id === question.id, @@ -83,6 +85,7 @@ export function QuestionInput({ withLabel: false, clearError, index, + patientId, }; switch (question.type) { diff --git a/src/components/Questionnaire/QuestionnaireForm.tsx b/src/components/Questionnaire/QuestionnaireForm.tsx index ad9e802ade0..8c7c02b5613 100644 --- a/src/components/Questionnaire/QuestionnaireForm.tsx +++ b/src/components/Questionnaire/QuestionnaireForm.tsx @@ -350,6 +350,7 @@ export function QuestionnaireForm({ disabled={isProcessing} activeGroupId={activeGroupId} errors={form.errors} + patientId={patientId} clearError={(questionId: string) => { setQuestionnaireForms((prev) => prev.map((f) => diff --git a/src/components/Questionnaire/structured/handlers.ts b/src/components/Questionnaire/structured/handlers.ts index 380860be3e6..5d11c1b7359 100644 --- a/src/components/Questionnaire/structured/handlers.ts +++ b/src/components/Questionnaire/structured/handlers.ts @@ -27,27 +27,21 @@ const handlers: { [K in StructuredQuestionType]: StructuredHandler; } = { allergy_intolerance: { - getRequests: (allergies, { patientId, encounterId }) => - allergies.map((allergy) => { - // Ensure all required fields have default values - const body: RequestTypeFor<"allergy_intolerance"> = { - clinical_status: allergy.clinical_status ?? "active", - verification_status: allergy.verification_status ?? "unconfirmed", - category: allergy.category ?? "medication", - criticality: allergy.criticality ?? "low", - code: allergy.code, - last_occurrence: allergy.last_occurrence, - note: allergy.note, - encounter: encounterId, - }; - - return { - url: `/api/v1/patient/${patientId}/allergy_intolerance/`, + getRequests: (allergies, { patientId, encounterId }) => { + return [ + { + url: `/api/v1/patient/${patientId}/allergy_intolerance/upsert/`, method: "POST", - body, + body: { + datapoints: allergies.map((allergy) => ({ + ...allergy, + encounter: encounterId, + })), + }, reference_id: "allergy_intolerance", - }; - }), + }, + ]; + }, }, medication_request: { getRequests: (medications, { patientId, encounterId }) => { diff --git a/src/components/Questionnaire/structured/types.ts b/src/components/Questionnaire/structured/types.ts index 6995831cd6c..7f31f61ad4c 100644 --- a/src/components/Questionnaire/structured/types.ts +++ b/src/components/Questionnaire/structured/types.ts @@ -3,10 +3,7 @@ import { FollowUpAppointmentRequest, } from "@/components/Schedule/types"; -import { - AllergyIntolerance, - AllergyIntoleranceRequest, -} from "@/types/emr/allergyIntolerance/allergyIntolerance"; +import { AllergyIntoleranceRequest } from "@/types/emr/allergyIntolerance/allergyIntolerance"; import { Diagnosis, DiagnosisRequest } from "@/types/emr/diagnosis/diagnosis"; import { Encounter, EncounterEditRequest } from "@/types/emr/encounter"; import { MedicationRequest } from "@/types/emr/medicationRequest"; @@ -16,7 +13,7 @@ import { StructuredQuestionType } from "@/types/questionnaire/question"; // Map structured types to their data types export interface StructuredDataMap { - allergy_intolerance: AllergyIntolerance; + allergy_intolerance: AllergyIntoleranceRequest; medication_request: MedicationRequest; medication_statement: MedicationStatement; symptom: Symptom; @@ -27,7 +24,7 @@ export interface StructuredDataMap { // Map structured types to their request types export interface StructuredRequestMap { - allergy_intolerance: AllergyIntoleranceRequest; + allergy_intolerance: { datapoints: AllergyIntoleranceRequest[] }; medication_request: { datapoints: MedicationRequest[] }; medication_statement: { datapoints: MedicationStatement[] }; symptom: SymptomRequest; diff --git a/src/pages/Encounters/tabs/EncounterUpdatesTab.tsx b/src/pages/Encounters/tabs/EncounterUpdatesTab.tsx index fb49046d1cb..72de24a181b 100644 --- a/src/pages/Encounters/tabs/EncounterUpdatesTab.tsx +++ b/src/pages/Encounters/tabs/EncounterUpdatesTab.tsx @@ -1,5 +1,6 @@ import ObservationsList from "@/components/Facility/ConsultationDetails/ObservationsList"; import QuestionnaireResponsesList from "@/components/Facility/ConsultationDetails/QuestionnaireResponsesList"; +import { AllergyList } from "@/components/Patient/allergy/list"; import { DiagnosisList } from "@/components/Patient/diagnosis/list"; import { SymptomsList } from "@/components/Patient/symptoms/list"; @@ -15,6 +16,11 @@ export const EncounterUpdatesTab = ({
{/* Left Column - Symptoms, Diagnoses, and Questionnaire Responses */}
+ {/* Allergies Section */} +
+ +
+ {/* Symptoms Section */}
diff --git a/src/types/emr/allergyIntolerance/allergyIntolerance.ts b/src/types/emr/allergyIntolerance/allergyIntolerance.ts index 915e5e0be09..860cfe14243 100644 --- a/src/types/emr/allergyIntolerance/allergyIntolerance.ts +++ b/src/types/emr/allergyIntolerance/allergyIntolerance.ts @@ -3,11 +3,12 @@ import { UserBase } from "../../user/user"; // Base type for allergy data export interface AllergyIntolerance { + id: string; code: Code; - clinical_status?: string; - verification_status?: string; - category?: string; - criticality?: string; + clinical_status: string; + verification_status: string; + category: string; + criticality: string; last_occurrence?: string; note?: string; created_by: UserBase; @@ -16,7 +17,9 @@ export interface AllergyIntolerance { } // Type for API request, extends base type with required fields +// Added optional id here as this type is used only in one place export interface AllergyIntoleranceRequest { + id?: string; clinical_status: string; verification_status: string; category: string; diff --git a/src/types/questionnaire/form.ts b/src/types/questionnaire/form.ts index eedd90c49b2..61df21fe62a 100644 --- a/src/types/questionnaire/form.ts +++ b/src/types/questionnaire/form.ts @@ -1,6 +1,6 @@ import { FollowUpAppointmentRequest } from "@/components/Schedule/types"; -import { AllergyIntolerance } from "@/types/emr/allergyIntolerance/allergyIntolerance"; +import { AllergyIntoleranceRequest } from "@/types/emr/allergyIntolerance/allergyIntolerance"; import { Diagnosis } from "@/types/emr/diagnosis/diagnosis"; import { Encounter } from "@/types/emr/encounter"; import { MedicationRequest } from "@/types/emr/medicationRequest"; @@ -29,7 +29,7 @@ export type ResponseValue = { | number | boolean | Date - | AllergyIntolerance[] + | AllergyIntoleranceRequest[] | MedicationRequest[] | MedicationStatement[] | Symptom[]