diff --git a/public/locale/en.json b/public/locale/en.json index 3f6aac87f3c..40bf5df4216 100644 --- a/public/locale/en.json +++ b/public/locale/en.json @@ -493,6 +493,7 @@ "bed_type__500": "Others", "before": "before", "beta": "beta", + "biologic": "Biologic", "bladder": "Bladder", "blood_group": "Blood Group", "blood_group_is_required": "Blood group is required", @@ -989,6 +990,7 @@ "enter_year_of_birth_to_verify": "Enter year of birth to verify", "entered_in_error": "Entered in Error", "entered_in_error_warning": "This action cannot be undone. The appointment will be marked as entered in error and removed from the system.", + "environment": "Environment", "error_404": "Error 404", "error_deleting_shifting": "Error while deleting Shifting record", "error_fetching_facility_data": "Error while fetching facility data", @@ -1078,6 +1080,7 @@ "filter_by_date": "Filter by Date", "filters": "Filters", "first_name": "First Name", + "food": "Food", "footer_body": "Open Healthcare Network is an open-source public utility designed by a multi-disciplinary team of innovators and volunteers. Open Healthcare Network CARE is a Digital Public Good recognised by the United Nations.", "forget_password": "Forgot password?", "forget_password_instruction": "Enter your username, and if it exists, we will send you a link to reset your password.", @@ -2109,6 +2112,7 @@ "type_to_search": "Type to search", "type_your_comment": "Type your comment", "type_your_reason_here": "Type your reason here", + "unable_to_assess": "Unable to Assess", "unable_to_get_current_position": "Unable to get current position.", "unable_to_get_location: ": "Unable to get location: ", "unassign": "Unassign", @@ -2260,6 +2264,7 @@ "video_conference_link": "Video Conference Link", "view": "View", "view_abdm_records": "View ABDM Records", + "view_all": "view all", "view_all_details": "View All Details", "view_and_manage_patient_encounters": "View and manage patient encounters", "view_asset": "View Assets", diff --git a/src/components/Common/ComboboxQuantityInput.tsx b/src/components/Common/ComboboxQuantityInput.tsx index db7b8caa4d2..19674422d81 100644 --- a/src/components/Common/ComboboxQuantityInput.tsx +++ b/src/components/Common/ComboboxQuantityInput.tsx @@ -50,6 +50,7 @@ export function ComboboxQuantityInput({ const showDropdown = /^\d+$/.test(inputValue); const handleInputChange = (e: React.ChangeEvent) => { + if (disabled) return; const value = e.target.value; if (value === "" || /^\d+$/.test(value)) { setInputValue(value); @@ -65,7 +66,7 @@ export function ComboboxQuantityInput({ }; const handleKeyDown = (e: React.KeyboardEvent) => { - if (!showDropdown) return; + if (disabled || !showDropdown) return; if (e.key === "ArrowDown") { e.preventDefault(); @@ -102,7 +103,7 @@ export function ComboboxQuantityInput({ return (
- +
+ + + +

{statement.medication.display ?? statement.medication.code}

+
+ +

{statement.medication.display ?? statement.medication.code}

+
+
+
+ + + +

{statement.dosage_text}

+
+ +

{statement.dosage_text}

+
+
+
+ + + {statement.status} + + + + {[statement.effective_period?.start, statement.effective_period?.end] + .map((date) => formatDateTime(date)) + .join(" - ")} + + + + +

{statement.reason}

+
+ +

{statement.reason}

+
+
+
+ + + +

{statement.note}

+
+ +

{statement.note}

+
+
+
+ + ); +} + export function MedicationStatementList({ patientId, }: MedicationStatementListProps) { const { t } = useTranslation(); + const [showEnteredInError, setShowEnteredInError] = useState(false); const { data: medications, isLoading } = useQuery({ queryKey: ["medication_statement", patientId], @@ -53,7 +127,16 @@ export function MedicationStatementList({ ); } - if (!medications?.results?.length) { + const filteredMedications = medications?.results?.filter( + (medication) => + showEnteredInError || medication.status !== "entered_in_error", + ); + + const hasEnteredInErrorRecords = medications?.results?.some( + (medication) => medication.status === "entered_in_error", + ); + + if (!filteredMedications?.length) { return ( @@ -66,22 +149,11 @@ export function MedicationStatementList({ ); } - const getStatusBadgeStyle = (status: string | undefined) => { - switch (status?.toLowerCase()) { - case "active": - return "bg-green-100 text-green-800 border-green-200"; - case "on_hold": - return "bg-gray-100 text-gray-800 border-blue-200"; - default: - return "bg-gray-100 text-gray-800 border-gray-200"; - } - }; - return ( - {t("ongoing_medications")} ({medications.count}) + {t("ongoing_medications")} ({filteredMedications.length}) @@ -97,77 +169,34 @@ export function MedicationStatementList({ - {medications.results.map((statement, index) => ( - - - - -

- {statement.medication.display ?? - statement.medication.code} -

-
- -

- {statement.medication.display ?? - statement.medication.code} -

-
-
-
- - - -

{statement.dosage_text}

-
- -

{statement.dosage_text}

-
-
-
- - - {statement.status} - - - - {[ - statement.effective_period?.start, - statement.effective_period?.end, - ] - .map((date) => formatDateTime(date)) - .join(" - ")} - - - - -

{statement.reason ?? "-"}

-
- -

{statement.reason ?? "Not Specified"}

-
-
-
- - - -

{statement.note ?? "-"}

-
- -

{statement.note ?? "Not Specified"}

-
-
-
-
- ))} + {filteredMedications.map((statement, index) => { + const isEnteredInError = statement.status === "entered_in_error"; + + return ( + <> + + + ); + })}
+ {hasEnteredInErrorRecords && !showEnteredInError && ( +
+ +
+ )}
); diff --git a/src/components/Patient/allergy/list.tsx b/src/components/Patient/allergy/list.tsx index 4034143c83d..8e1a9686a90 100644 --- a/src/components/Patient/allergy/list.tsx +++ b/src/components/Patient/allergy/list.tsx @@ -2,9 +2,10 @@ import { useQuery } from "@tanstack/react-query"; import { t } from "i18next"; import { PencilIcon } from "lucide-react"; import { Link } from "raviger"; -import { ReactNode } from "react"; +import { ReactNode, useState } from "react"; import { Badge } from "@/components/ui/badge"; +import { Button } from "@/components/ui/button"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Skeleton } from "@/components/ui/skeleton"; import { @@ -19,7 +20,6 @@ import { import { Avatar } from "@/components/Common/Avatar"; import query from "@/Utils/request/query"; -import { formatName } from "@/Utils/utils"; import { AllergyIntolerance } from "@/types/emr/allergyIntolerance/allergyIntolerance"; import allergyIntoleranceApi from "@/types/emr/allergyIntolerance/allergyIntoleranceApi"; @@ -34,6 +34,8 @@ export function AllergyList({ patientId, encounterId, }: AllergyListProps) { + const [showEnteredInError, setShowEnteredInError] = useState(false); + const { data: allergies, isLoading } = useQuery({ queryKey: ["allergies", patientId, encounterId], queryFn: query(allergyIntoleranceApi.getAllergy, { @@ -49,21 +51,30 @@ export function AllergyList({ patientId={patientId} encounterId={encounterId} > - + ); } - if (!allergies?.results?.length) { + const filteredAllergies = allergies?.results?.filter( + (allergy) => + showEnteredInError || allergy.verification_status !== "entered_in_error", + ); + + const hasEnteredInErrorRecords = allergies?.results?.some( + (allergy) => allergy.verification_status === "entered_in_error", + ); + + if (!filteredAllergies?.length) { return ( - +

{t("no_allergies_recorded")}

@@ -94,6 +105,62 @@ export function AllergyList({ } }; + interface AllergyRowProps { + allergy: AllergyIntolerance; + isEnteredInError?: boolean; + } + + function AllergyRow({ allergy, isEnteredInError }: AllergyRowProps) { + return ( + + {allergy.code.display} + + + {t(allergy.category)} + + + + + {t(allergy.clinical_status)} + + + + + {t(allergy.criticality)} + + + + + {t(allergy.verification_status)} + + + + + {allergy.created_by.username} + + + ); + } + return ( {t("category")} {t("status")} {t("criticality")} + {t("verification")} {t("created_by")} - {allergies.results - .sort((a, _b) => { - if (a.clinical_status === "inactive") { - return 1; - } - return -1; - }) - .map((allergy: AllergyIntolerance) => ( - - - {allergy.code.display} - - - - {allergy.category} - - - - - {allergy.clinical_status} - - - - - {allergy.criticality} - - - - - - {formatName(allergy.created_by)} - - - + {/* Valid entries */} + {filteredAllergies + .filter( + (allergy) => allergy.verification_status !== "entered_in_error", + ) + .map((allergy) => ( + ))} + + {/* Entered in error entries */} + {showEnteredInError && + filteredAllergies + .filter( + (allergy) => allergy.verification_status === "entered_in_error", + ) + .map((allergy) => ( + + ))} + {hasEnteredInErrorRecords && !showEnteredInError && ( +
+ +
+ )}
); } @@ -191,7 +244,7 @@ const AllergyListLayout = ({ )}
- {children} + {children}
); }; diff --git a/src/components/Patient/diagnosis/DiagnosisTable.tsx b/src/components/Patient/diagnosis/DiagnosisTable.tsx index 4646c028a9e..d9134f6d0c8 100644 --- a/src/components/Patient/diagnosis/DiagnosisTable.tsx +++ b/src/components/Patient/diagnosis/DiagnosisTable.tsx @@ -54,46 +54,61 @@ export function DiagnosisTable({ )} - {diagnoses.map((diagnosis: Diagnosis, index) => ( - - - {diagnosis.code.display} - - - { + const isEnteredInError = + diagnosis.verification_status === "entered_in_error"; + + return ( + <> + - {diagnosis.clinical_status} - - - - - {diagnosis.verification_status} - - - - {diagnosis.onset?.onset_datetime - ? new Date(diagnosis.onset.onset_datetime).toLocaleDateString() - : "-"} - - - {diagnosis.note || "-"} - - -
- - - {formatName(diagnosis.created_by)} - -
-
-
- ))} + + {diagnosis.code.display} + + + + {t(diagnosis.clinical_status)} + + + + + {t(diagnosis.verification_status)} + + + + {diagnosis.onset?.onset_datetime + ? new Date( + diagnosis.onset.onset_datetime, + ).toLocaleDateString() + : "-"} + + + {diagnosis.note || "-"} + + + + + {formatName(diagnosis.created_by)} + + + + + ); + })}
); diff --git a/src/components/Patient/diagnosis/list.tsx b/src/components/Patient/diagnosis/list.tsx index cf1e375fd58..d0e8c4acede 100644 --- a/src/components/Patient/diagnosis/list.tsx +++ b/src/components/Patient/diagnosis/list.tsx @@ -2,8 +2,9 @@ import { useQuery } from "@tanstack/react-query"; import { t } from "i18next"; import { PencilIcon } from "lucide-react"; import { Link } from "raviger"; -import { ReactNode } from "react"; +import { ReactNode, useState } from "react"; +import { Button } from "@/components/ui/button"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Skeleton } from "@/components/ui/skeleton"; @@ -23,6 +24,8 @@ export function DiagnosisList({ encounterId, facilityId, }: DiagnosisListProps) { + const [showEnteredInError, setShowEnteredInError] = useState(false); + const { data: diagnoses, isLoading } = useQuery({ queryKey: ["diagnosis", patientId, encounterId], queryFn: query(diagnosisApi.listDiagnosis, { @@ -43,14 +46,24 @@ export function DiagnosisList({ ); } - if (!diagnoses?.results?.length) { + const filteredDiagnoses = diagnoses?.results?.filter( + (diagnosis) => + showEnteredInError || + diagnosis.verification_status !== "entered_in_error", + ); + + const hasEnteredInErrorRecords = diagnoses?.results?.some( + (diagnosis) => diagnosis.verification_status === "entered_in_error", + ); + + if (!filteredDiagnoses?.length) { return ( -

No diagnoses recorded

+

{t("no_diagnoses_recorded")}

); } @@ -61,7 +74,32 @@ export function DiagnosisList({ patientId={patientId} encounterId={encounterId} > - + diagnosis.verification_status !== "entered_in_error", + ), + ...(showEnteredInError + ? filteredDiagnoses.filter( + (diagnosis) => + diagnosis.verification_status === "entered_in_error", + ) + : []), + ]} + /> + + {hasEnteredInErrorRecords && !showEnteredInError && ( +
+ +
+ )} ); } @@ -91,7 +129,7 @@ const DiagnosisListLayout = ({ )} - {children} + {children} ); }; diff --git a/src/components/Patient/symptoms/SymptomTable.tsx b/src/components/Patient/symptoms/SymptomTable.tsx index c9c10266368..b95e518d99d 100644 --- a/src/components/Patient/symptoms/SymptomTable.tsx +++ b/src/components/Patient/symptoms/SymptomTable.tsx @@ -48,52 +48,73 @@ export function SymptomTable({ {t("status")} {t("severity")} {t("onset")} + {t("verification")} {t("notes")} {t("created_by")} )} - {symptoms.map((symptom: Symptom, index: number) => ( - - - {symptom.code.display} - - - { + const isEnteredInError = + symptom.verification_status === "entered_in_error"; + + return ( + <> + - {symptom.clinical_status} - - - - - {symptom.severity} - - - - {symptom.onset?.onset_datetime - ? new Date(symptom.onset.onset_datetime).toLocaleDateString() - : "-"} - - - {symptom.note || "-"} - - -
- - - {formatName(symptom.created_by)} - -
-
-
- ))} + + {symptom.code.display} + + + + {t(symptom.clinical_status)} + + + + + {t(symptom.severity)} + + + + {symptom.onset?.onset_datetime + ? new Date( + symptom.onset.onset_datetime, + ).toLocaleDateString() + : "-"} + + + + {t(symptom.verification_status)} + + + + {symptom.note || "-"} + + + + + {formatName(symptom.created_by)} + + + + + ); + })}
); diff --git a/src/components/Patient/symptoms/list.tsx b/src/components/Patient/symptoms/list.tsx index 64100b7ca1f..24f7a8142c8 100644 --- a/src/components/Patient/symptoms/list.tsx +++ b/src/components/Patient/symptoms/list.tsx @@ -2,8 +2,9 @@ import { useQuery } from "@tanstack/react-query"; import { t } from "i18next"; import { PencilIcon } from "lucide-react"; import { Link } from "raviger"; -import { ReactNode } from "react"; +import { ReactNode, useState } from "react"; +import { Button } from "@/components/ui/button"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Skeleton } from "@/components/ui/skeleton"; @@ -23,6 +24,8 @@ export function SymptomsList({ encounterId, facilityId, }: SymptomsListProps) { + const [showEnteredInError, setShowEnteredInError] = useState(false); + const { data: symptoms, isLoading } = useQuery({ queryKey: ["symptoms", patientId, encounterId], queryFn: query(symptomApi.listSymptoms, { @@ -43,14 +46,23 @@ export function SymptomsList({ ); } - if (!symptoms?.results?.length) { + const filteredSymptoms = symptoms?.results?.filter( + (symptom) => + showEnteredInError || symptom.verification_status !== "entered_in_error", + ); + + const hasEnteredInErrorRecords = symptoms?.results?.some( + (symptom) => symptom.verification_status === "entered_in_error", + ); + + if (!filteredSymptoms?.length) { return ( -

No symptoms recorded

+

{t("no_symptoms_recorded")}

); } @@ -61,7 +73,31 @@ export function SymptomsList({ patientId={patientId} encounterId={encounterId} > - + symptom.verification_status !== "entered_in_error", + ), + ...(showEnteredInError + ? filteredSymptoms.filter( + (symptom) => symptom.verification_status === "entered_in_error", + ) + : []), + ]} + /> + + {hasEnteredInErrorRecords && !showEnteredInError && ( +
+ +
+ )} ); } @@ -91,7 +127,7 @@ const SymptomListLayout = ({ )} - {children} + {children} ); }; diff --git a/src/components/Questionnaire/QuestionRenderer.tsx b/src/components/Questionnaire/QuestionRenderer.tsx index b4f5168c60e..d63aa0e4cf7 100644 --- a/src/components/Questionnaire/QuestionRenderer.tsx +++ b/src/components/Questionnaire/QuestionRenderer.tsx @@ -3,7 +3,10 @@ import { useEffect, useRef } from "react"; import { cn } from "@/lib/utils"; import { QuestionValidationError } from "@/types/questionnaire/batch"; -import { QuestionnaireResponse } from "@/types/questionnaire/form"; +import { + QuestionnaireResponse, + ResponseValue, +} from "@/types/questionnaire/form"; import { Question, StructuredQuestionType, @@ -20,7 +23,7 @@ const FULL_WIDTH_QUESTION_TYPES: StructuredQuestionType[] = [ interface QuestionRendererProps { questions: Question[]; responses: QuestionnaireResponse[]; - onResponseChange: (responses: QuestionnaireResponse[]) => void; + onResponseChange: (values: ResponseValue[], questionId: string) => void; errors: QuestionValidationError[]; clearError: (questionId: string) => void; disabled?: boolean; @@ -53,18 +56,12 @@ export function QuestionRenderer({ } }, [activeGroupId]); - const handleResponseChange = (updatedResponse: QuestionnaireResponse) => { - const newResponses = [...responses]; - const index = newResponses.findIndex( - (r) => r.question_id === updatedResponse.question_id, - ); - if (index !== -1) { - newResponses[index] = updatedResponse; - } else { - newResponses.push(updatedResponse); - } - onResponseChange(newResponses); - }; + // const handleResponseChange = ( + // updatedResponse: ResponseValue[], + // questionId: string, + // ) => { + // onResponseChange(newResponses, questionId); + // }; const shouldBeFullWidth = (question: Question): boolean => question.type === "structured" && @@ -86,7 +83,7 @@ export function QuestionRenderer({ question={question} encounterId={encounterId} questionnaireResponses={responses} - updateQuestionnaireResponseCB={handleResponseChange} + updateQuestionnaireResponseCB={onResponseChange} errors={errors} clearError={clearError} disabled={disabled} diff --git a/src/components/Questionnaire/QuestionTypes/AllergyQuestion.tsx b/src/components/Questionnaire/QuestionTypes/AllergyQuestion.tsx index 8ccc6512821..8270ae98c71 100644 --- a/src/components/Questionnaire/QuestionTypes/AllergyQuestion.tsx +++ b/src/components/Questionnaire/QuestionTypes/AllergyQuestion.tsx @@ -53,14 +53,21 @@ import { } 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 { + QuestionnaireResponse, + ResponseValue, +} from "@/types/questionnaire/form"; import { Question } from "@/types/questionnaire/question"; interface AllergyQuestionProps { patientId: string; question: Question; questionnaireResponse: QuestionnaireResponse; - updateQuestionnaireResponseCB: (response: QuestionnaireResponse) => void; + updateQuestionnaireResponseCB: ( + values: ResponseValue[], + questionId: string, + note?: string, + ) => void; disabled?: boolean; } @@ -126,15 +133,15 @@ export function AllergyQuestion({ useEffect(() => { if (patientAllergies?.results) { - updateQuestionnaireResponseCB({ - ...questionnaireResponse, - values: [ + updateQuestionnaireResponseCB( + [ { type: "allergy_intolerance", value: patientAllergies.results.map(convertToAllergyRequest), }, ], - }); + questionnaireResponse.question_id, + ); } }, [patientAllergies]); @@ -143,18 +150,43 @@ export function AllergyQuestion({ ...allergies, { ...ALLERGY_INITIAL_VALUE, code }, ] as AllergyIntoleranceRequest[]; - updateQuestionnaireResponseCB({ - ...questionnaireResponse, - values: [{ type: "allergy_intolerance", value: newAllergies }], - }); + updateQuestionnaireResponseCB( + [{ type: "allergy_intolerance", value: newAllergies }], + questionnaireResponse.question_id, + ); }; const handleRemoveAllergy = (index: number) => { - const newAllergies = allergies.filter((_, i) => i !== index); - updateQuestionnaireResponseCB({ - ...questionnaireResponse, - values: [{ type: "allergy_intolerance", value: newAllergies }], - }); + const allergy = allergies[index]; + if (allergy.id) { + // For existing records, update verification status to entered_in_error + const newAllergies = allergies.map((a, i) => + i === index + ? { ...a, verification_status: "entered_in_error" as const } + : a, + ) as AllergyIntoleranceRequest[]; + updateQuestionnaireResponseCB( + [ + { + type: "allergy_intolerance", + value: newAllergies, + }, + ], + questionnaireResponse.question_id, + ); + } else { + // For new records, remove them completely + const newAllergies = allergies.filter((_, i) => i !== index); + updateQuestionnaireResponseCB( + [ + { + type: "allergy_intolerance", + value: newAllergies, + }, + ], + questionnaireResponse.question_id, + ); + } }; const handleUpdateAllergy = ( @@ -164,10 +196,10 @@ export function AllergyQuestion({ const newAllergies = allergies.map((allergy, i) => i === index ? { ...allergy, ...updates } : allergy, ); - updateQuestionnaireResponseCB({ - ...questionnaireResponse, - values: [{ type: "allergy_intolerance", value: newAllergies }], - }); + updateQuestionnaireResponseCB( + [{ type: "allergy_intolerance", value: newAllergies }], + questionnaireResponse.question_id, + ); }; return ( @@ -211,11 +243,13 @@ export function AllergyQuestion({
@@ -225,7 +259,7 @@ export function AllergyQuestion({ onValueChange={(value) => handleUpdateAllergy(index, { category: value }) } - disabled={disabled} + disabled={disabled || !!allergy.id} > @@ -430,11 +464,13 @@ const AllergyTableRow = ({ const [showNotes, setShowNotes] = useState(allergy.note !== undefined); const rowClassName = `group ${ - allergy.clinical_status === "inactive" - ? "opacity-60" - : allergy.clinical_status === "resolved" - ? "line-through" - : "" + allergy.verification_status === "entered_in_error" + ? "opacity-40 pointer-events-none" + : allergy.clinical_status === "inactive" + ? "opacity-60" + : allergy.clinical_status === "resolved" + ? "line-through" + : "" }`; const handleNotesToggle = () => { @@ -454,7 +490,7 @@ const AllergyTableRow = ({