From 85866daafa53a7c8dd237631a2bfab5da099c5fe Mon Sep 17 00:00:00 2001 From: Amjith Titus Date: Thu, 6 Feb 2025 15:19:29 +0530 Subject: [PATCH] Encounter overview and sidebar Redesign (#10440) --- public/locale/en.json | 35 ++- .../ConsultationDetails/ObservationsList.tsx | 134 ++++++++--- .../ConsultationDetails/OverviewSideBar.tsx | 40 +++ .../ConsultationDetails/QuickAccess.tsx | 180 ++++++++++++++ src/components/Patient/PatientInfoCard.tsx | 26 +- src/components/Patient/allergy/list.tsx | 227 ++++++++++++------ .../Patient/diagnosis/DiagnosisTable.tsx | 200 ++++++++------- src/components/Patient/diagnosis/list.tsx | 38 +-- .../Patient/symptoms/SymptomTable.tsx | 212 +++++++++------- src/components/Patient/symptoms/list.tsx | 40 +-- .../QuestionTypes/AllergyQuestion.tsx | 82 ++++--- .../Encounters/tabs/EncounterUpdatesTab.tsx | 6 +- .../allergyIntolerance/allergyIntolerance.ts | 22 +- src/types/emr/diagnosis/diagnosis.ts | 18 ++ src/types/emr/symptom/symptom.ts | 24 ++ 15 files changed, 924 insertions(+), 360 deletions(-) create mode 100644 src/components/Facility/ConsultationDetails/OverviewSideBar.tsx create mode 100644 src/components/Facility/ConsultationDetails/QuickAccess.tsx diff --git a/public/locale/en.json b/public/locale/en.json index 7e45f14d80f..7a455f5a214 100644 --- a/public/locale/en.json +++ b/public/locale/en.json @@ -304,6 +304,7 @@ "add_notes": "Add notes", "add_notes_about_diagnosis": "Add notes about the diagnosis...", "add_notes_about_symptom": "Add notes about the symptom...", + "add_notes_about_the_allergy": "Add notes about the allergy", "add_organizations": "Add Organizations", "add_patient_updates": "Add Patient Updates", "add_policy": "Add Insurance Policy", @@ -492,6 +493,7 @@ "cannot_select_year_out_of_range": "Cannot select year out of range", "capture": "Capture", "card": "Card", + "cardiology": "Cardiology", "care": "CARE", "care_backend": "Care Backend", "care_frontend": "Care Frontend", @@ -582,7 +584,9 @@ "confirm_transfer_complete": "Confirm Transfer Complete!", "confirm_unavailability": "Confirm Unavailability", "confirmed": "Confirmed", + "consent_form": "Consent Form", "consult": "Consult", + "consultation": "Consultation", "consultation_history": "Consultation History", "consultation_missing_warning": "You have not created a consultation for the patient in", "consultation_not_filed": "You have not filed a consultation for this patient yet.", @@ -690,6 +694,7 @@ "demography": "Demography", "denied_on": "Denied On", "departments": "Departments", + "departments_and_teams": "Departments and Teams", "describe_why_the_asset_is_not_working": "Describe why the asset is not working", "description": "Description", "details_about_the_equipment": "Details about the equipment", @@ -724,6 +729,7 @@ "discharged_on": "Discharged On", "discharged_patients": "Discharged Patients", "discharged_patients_empty": "No discharged patients present in this facility", + "discharged_to": "Discharged to", "disclaimer": "Disclaimer", "discontinue": "Discontinue", "discontinue_caution_note": "Are you sure you want to discontinue this prescription?", @@ -742,6 +748,7 @@ "doctor_nurse": "Doctor/Nurse", "doctor_s_medical_council_registration": "Doctor's Medical Council Registration", "doctors_name": "Doctor's Name", + "doctors_progress_note": "Doctor's Progress Note", "domestic_healthcare_support": "Domestic healthcare support", "domestic_international_travel": "Domestic/international Travel (within last 28 days)", "done": "Done", @@ -885,6 +892,9 @@ "encounter_priority__timing_critical": "Timing critical", "encounter_priority__urgent": "Urgent", "encounter_priority__use_as_directed": "Use as directed", + "encounter_re_admission__false": "No", + "encounter_re_admission__true": "Yes", + "encounter_settings": "Encounter Settings", "encounter_status": "Encounter Status", "encounter_status__cancelled": "Cancelled", "encounter_status__completed": "Completed", @@ -981,6 +991,7 @@ "facility_updated_successfully": "Facility updated successfully", "failed_to_create_appointment": "Failed to create an appointment", "failed_to_link_abha_number": "Failed to link ABHA Number. Please try again later.", + "false": "False", "fast_track_testing_reason": "Fast track testing reason", "features": "Features", "feed_configurations": "Feed Configurations", @@ -1029,6 +1040,7 @@ "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.", + "forms": "Forms", "frequency": "Frequency", "from": "from", "from_date_must_be_before_to_date": "From date must be before to date", @@ -1078,6 +1090,7 @@ "home_facility_updated_error": "Error while updating Home Facility", "home_facility_updated_success": "Home Facility updated successfully", "hospital_identifier": "Hospital Identifier", + "hospitalisation_details": "Hospitalization Details", "hospitalization_details": "Hospitalization Details", "hubs": "Hub Facilities", "i_declare": "I hereby declare that:", @@ -1225,6 +1238,7 @@ "log_report": "Log Report", "log_update": "Log Update", "log_updates": "Log Updates", + "logged_by": "Logged by", "logged_in_as": "Logged in as", "login": "Login", "logout": "Log Out", @@ -1247,6 +1261,7 @@ "manage_user": "Manage User", "manufacturer": "Manufacturer", "map_acronym": "M.A.P.", + "mark_active": "Mark Active", "mark_all_as_read": "Mark all as Read", "mark_as_complete": "Mark as Complete", "mark_as_entered_in_error": "Mark as entered in error", @@ -1254,6 +1269,8 @@ "mark_as_noshow": "Mark as no-show", "mark_as_read": "Mark as Read", "mark_as_unread": "Mark as Unread", + "mark_inactive": "Mark Inactive", + "mark_resolved": "Mark Resolved", "mark_this_transfer_as_complete_question": "Are you sure you want to mark this transfer as complete? The Origin facility will no longer have access to this patient", "mark_transfer_complete_confirmation": "Are you sure you want to mark this transfer as complete? The Origin facility will no longer have access to this patient", "markdown_supported": "You can use markdown to format your facility description", @@ -1435,12 +1452,16 @@ "number_of_chronic_diseased_dependents": "Number Of Chronic Diseased Dependents", "number_of_covid_vaccine_doses": "Number of Covid vaccine doses", "nurse": "Nurse", + "nurses_form": "Nurse's Form", "nursing_care": "Nursing Care", + "nursing_home": "Nursing Home", "nursing_information": "Nursing Information", "nutrition": "Nutrition", + "observations": "Observations", "occupancy": "Occupancy", "occupation": "Occupation", "occupied": "Occupied", + "occurrence": "Occurrence", "old_password": "Current Password", "on": "on", "on_emergency_basis": " on emergency basis", @@ -1669,12 +1690,14 @@ "questionnaire_not_exist": "The questionnaire you tried to access does not exist.", "questionnaire_submission_failed": "Failed to submit questionnaire", "questionnaire_submitted_successfully": "Questionnaire submitted successfully", + "quick_access": "Quick Access", "quick_actions": "Quick Actions", "quick_actions_description": "Schedule an appointment or create a new encounter", "ration_card__APL": "APL", "ration_card__BPL": "BPL", "ration_card__NO_CARD": "Non-card holder", "ration_card_category": "Ration Card Category", + "re_admission": "Re-Admission", "readmission": "Re-admission", "reason": "Reason", "reason_for_discontinuation": "Reason for discontinuation", @@ -1685,6 +1708,7 @@ "reason_for_shift": "Reason for shift", "reason_for_visit": "Reason for visit", "reason_for_visit_placeholder": "Type the reason for booking appointment", + "recommend_discharge": "Recommend Discharge", "recommended_aspect_ratio_for": "Recommended aspect ratio for the image is {{aspectRatio}}.", "record": "Record Audio", "record_delete_confirm": "Are you sure you want to delete this record?", @@ -1711,6 +1735,7 @@ "remarks_placeholder": "Enter remarks", "remission": "Remission", "remove": "Remove", + "remove_allergy": "Remove Allergy", "remove_diagnosis": "Remove Diagnosis", "remove_medication": "Remove Medication", "remove_medication_confirmation": "Are you sure you want to remove {{medication}}?", @@ -1776,6 +1801,7 @@ "resource_status__transportation_to_be_arranged": "Transportation to be arranged", "resource_title": "Resource Title", "resource_type": "Request Type", + "respiratory_status": "Respiratory Status", "result": "Result", "result_date": "Result Date", "result_details": "Result details", @@ -1849,6 +1875,7 @@ "search_by_username": "Search by username", "search_country": "Search country...", "search_encounters": "Search Encounters", + "search_for_allergies_to_add": "Search for allergies to add", "search_for_diagnoses_to_add": "Search for diagnoses to add", "search_for_facility": "Search for Facility", "search_for_symptoms_to_add": "Search for symptoms to add", @@ -1865,6 +1892,7 @@ "search_user_description": "Search for a user and assign a role to add them to the patient.", "searching": "Searching...", "see_attachments": "See Attachments", + "see_note": "See Note", "select": "Select", "select_additional_instructions": "Select additional instructions", "select_admit_source": "Select Admit Source", @@ -2035,6 +2063,7 @@ "tachycardia": "Tachycardia", "tag_name": "Tag Name", "tag_slug": "Tag Slug", + "tags": "Tags", "taken": "Taken", "taper_titrate_dosage": "Taper & Titrate Dosage", "target_dosage": "Target Dosage", @@ -2076,6 +2105,7 @@ "treatment_summary__heading": "INTERIM TREATMENT SUMMARY", "treatment_summary__print": "Print Treatment Summary", "triage_category": "Triage Category", + "true": "True", "try_again_later": "Try again later!", "try_different_abha_linking_option": "Want to try a different linking option, here are some more:", "type_any_extra_comments_here": "type any extra comments here", @@ -2142,9 +2172,12 @@ "update_asset_service_record": "Update Asset Service Record", "update_available": "Update Available", "update_bed": "Update Bed", + "update_department": "Update Department", + "update_encounter_details": "Update Encounter Details", "update_existing_facility": "Update the details of the existing facility.", "update_facility": "Update Facility", "update_facility_middleware_success": "Facility middleware updated successfully", + "update_hospitalisation_details": "Update Hospitalisation Details", "update_log": "Update Log", "update_password": "Update Password", "update_patient_details": "Update Patient Details", @@ -2239,7 +2272,7 @@ "video_conference_link": "Video Conference Link", "view": "View", "view_abdm_records": "View ABDM Records", - "view_all": "view all", + "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/Facility/ConsultationDetails/ObservationsList.tsx b/src/components/Facility/ConsultationDetails/ObservationsList.tsx index 980a6a1c0d6..e15278773bd 100644 --- a/src/components/Facility/ConsultationDetails/ObservationsList.tsx +++ b/src/components/Facility/ConsultationDetails/ObservationsList.tsx @@ -3,8 +3,6 @@ import { useEffect } from "react"; import { useTranslation } from "react-i18next"; import { useInView } from "react-intersection-observer"; -import CareIcon from "@/CAREUI/icons/CareIcon"; - import { Card } from "@/components/ui/card"; import { formatValue } from "@/components/Facility/ConsultationDetails/QuestionnaireResponsesList"; @@ -13,7 +11,6 @@ import routes from "@/Utils/request/api"; import query from "@/Utils/request/query"; import { HTTPError } from "@/Utils/request/types"; import { PaginatedResponse } from "@/Utils/request/types"; -import { formatDateTime } from "@/Utils/utils"; import { Encounter } from "@/types/emr/encounter"; import { Observation } from "@/types/emr/observation"; @@ -23,6 +20,59 @@ interface Props { encounter: Encounter; } +interface GroupedObservations { + [key: string]: Observation[]; +} + +function getDateKey(date: string) { + return new Date(date).toISOString().split("T")[0]; +} + +function formatDisplayDate(dateStr: string) { + const date = new Date(dateStr); + const today = new Date(); + const yesterday = new Date(today); + yesterday.setDate(yesterday.getDate() - 1); + + // Reset time parts for accurate date comparison + today.setHours(0, 0, 0, 0); + yesterday.setHours(0, 0, 0, 0); + date.setHours(0, 0, 0, 0); + + const formattedDate = date.toLocaleDateString("en-US", { + year: "numeric", + month: "long", + day: "numeric", + }); + + if (date.getTime() === today.getTime()) { + return `Today (${formattedDate})`; + } else if (date.getTime() === yesterday.getTime()) { + return `Yesterday (${formattedDate})`; + } + return formattedDate; +} + +function formatDisplayTime(dateStr: string) { + return new Date(dateStr).toLocaleTimeString("en-US", { + hour: "2-digit", + minute: "2-digit", + }); +} + +function groupObservationsByDate( + observations: Observation[], +): GroupedObservations { + return observations.reduce((groups: GroupedObservations, observation) => { + const dateKey = getDateKey(observation.effective_datetime); + if (!groups[dateKey]) { + groups[dateKey] = []; + } + groups[dateKey].push(observation); + return groups; + }, {}); +} + export default function ObservationsList(props: Props) { const { t } = useTranslation(); const patientId = props.encounter.patient.id; @@ -77,35 +127,63 @@ export default function ObservationsList(props: Props) { ); } + const groupedObservations = groupObservationsByDate(observations); + const dates = Object.keys(groupedObservations).sort().reverse(); + return (
- {observations.map((item: Observation) => ( - -
-
- - {formatDateTime(item.effective_datetime)} -
-
- {item.main_code.display || item.main_code.code} -
- {item.value.value_quantity && ( -
- {item.value.value_quantity.value}{" "} - {item.value.value_quantity.code.display} -
- )} - {item.value.value && ( -
- {formatValue(item.value.value, item.value_type)} -
- )} - {item.note && ( -
{item.note}
- )} + {dates.map((date, index) => ( +
+
+ {formatDisplayDate(date)}
- +
+ {groupedObservations[date] + .sort( + (a, b) => + new Date(b.effective_datetime).getTime() - + new Date(a.effective_datetime).getTime(), + ) + .map((item: Observation) => ( +
+
+ {formatDisplayTime(item.effective_datetime)}: +
+ +
+
+ {item.value.value && ( +
+ {formatValue(item.value.value, item.value_type)} +
+ )} + {item.value.value_quantity && ( +
+ {item.value.value_quantity.value}{" "} +
+ {item.value.value_quantity.code.display} +
+
+ )} +
+ {item.note && ( +
+ {item.note} +
+ )} +
+ {item.main_code.display || item.main_code.code} +
+
+
+
+ ))} +
+ {index < dates.length - 1 && ( +
+ )} +
))} {hasNextPage && (
diff --git a/src/components/Facility/ConsultationDetails/OverviewSideBar.tsx b/src/components/Facility/ConsultationDetails/OverviewSideBar.tsx new file mode 100644 index 00000000000..f59ba6e3da1 --- /dev/null +++ b/src/components/Facility/ConsultationDetails/OverviewSideBar.tsx @@ -0,0 +1,40 @@ +import { t } from "i18next"; + +import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; + +import { Encounter } from "@/types/emr/encounter"; + +import ObservationsList from "./ObservationsList"; +import QuickAccess from "./QuickAccess"; + +interface Props { + encounter: Encounter; +} + +export default function SideOverview(props: Props) { + return ( +
+ +
+ + + {t("quick_access")} + + + {t("observations")} + + +
+ +
+ + + + + + +
+
+
+ ); +} diff --git a/src/components/Facility/ConsultationDetails/QuickAccess.tsx b/src/components/Facility/ConsultationDetails/QuickAccess.tsx new file mode 100644 index 00000000000..b9aa70c903d --- /dev/null +++ b/src/components/Facility/ConsultationDetails/QuickAccess.tsx @@ -0,0 +1,180 @@ +import { useQuery } from "@tanstack/react-query"; +import { Link } from "raviger"; +import { useTranslation } from "react-i18next"; + +import CareIcon from "@/CAREUI/icons/CareIcon"; + +import { Badge } from "@/components/ui/badge"; +import { Button } from "@/components/ui/button"; + +import LinkDepartmentsSheet from "@/components/Patient/LinkDepartmentsSheet"; + +import query from "@/Utils/request/query"; +import { Encounter } from "@/types/emr/encounter"; +import questionnaireApi from "@/types/questionnaire/questionnaireApi"; + +interface QuickAccessProps { + encounter: Encounter; +} + +export default function QuickAccess({ encounter }: QuickAccessProps) { + const { t } = useTranslation(); + + const { data: response } = useQuery({ + queryKey: ["questionnaires"], + queryFn: query(questionnaireApi.list), + }); + + const questionnaireList = response?.results || []; + + const encounterSettings = [ + { id: "encounter_settings", label: t("encounter_settings") }, + ]; + + return ( +
+ {/* Questionnaire Section */} +
+

{t("questionnaire")}

+
+ {questionnaireList.map((item) => ( + + + {item.title} + + ))} +
+
+ +
+ + {/* Update Encounter Details */} +
+

+ {t("update_encounter_details")} +

+
+ {encounterSettings.map((item) => ( +
+ + {item.label} + +
+ ))} +
+
+ +
+ + {/* Departments and Teams */} +
+
+

+ {t("departments_and_teams")} +

+ + +
+ } + /> +
+
+ + {encounter.organizations.length > 0 + ? encounter.organizations.map((org) => ( + + {org.name} + + )) + : t("no_organizations_added_yet")} +
+ } + /> +
+ + + {encounter.hospitalization?.admit_source && ( + <> +
+ + {/* Hospitalisation Details */} +
+

+ {t("hospitalisation_details")} +

+
+
+ {t("admit_source")} + + {t( + `encounter_admit_sources__${encounter.hospitalization?.admit_source}`, + )} + +
+
+ {t("diet_preference")} + + {t( + `encounter_diet_preference__${encounter.hospitalization?.diet_preference}`, + )} + +
+
+ {t("re_admission")} + + {t( + `encounter_re_admission__${encounter.hospitalization?.re_admission}`, + )} + +
+ +
+
+ + )} +
+ ); +} diff --git a/src/components/Patient/PatientInfoCard.tsx b/src/components/Patient/PatientInfoCard.tsx index f73d9fbab3f..9ea75adc2c8 100644 --- a/src/components/Patient/PatientInfoCard.tsx +++ b/src/components/Patient/PatientInfoCard.tsx @@ -12,6 +12,8 @@ import { Link } from "raviger"; import { useTranslation } from "react-i18next"; import { toast } from "sonner"; +import CareIcon from "@/CAREUI/icons/CareIcon"; + import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { @@ -265,11 +267,25 @@ export default function PatientInfoCard(props: PatientInfoCardProps) { )} - {encounter.hospitalization?.discharge_disposition && ( - - {encounter.hospitalization.discharge_disposition} - - )} + { + // (encounter.status === "discharged" || + // encounter.status === "completed") && + encounter.hospitalization?.discharge_disposition && ( + + + {t( + `encounter_discharge_disposition__${encounter.hospitalization.discharge_disposition}`, + )} + + ) + } = { + food: , + medication: ( + + ), + environment: ( + + ), + biologic: ( + + ), +}; + export function AllergyList({ facilityId, patientId, @@ -88,81 +120,106 @@ export function AllergyList({ ); } - const getStatusBadgeStyle = (status: string | undefined) => { - switch (status?.toLowerCase()) { - case "active": - return "bg-green-100 text-green-800 border-green-200"; - case "inactive": - return "bg-gray-100 text-gray-800 border-gray-200"; - default: - return "bg-gray-100 text-gray-800 border-gray-200"; - } - }; - - const getCategoryBadgeStyle = (category: string) => { - switch (category?.toLowerCase()) { - case "food": - return "bg-orange-100 text-orange-800 border-orange-200"; - case "medication": - return "bg-blue-100 text-blue-800 border-blue-200"; - case "environment": - return "bg-green-100 text-green-800 border-green-200"; - default: - return "bg-gray-100 text-gray-800 border-gray-200"; - } - }; - interface AllergyRowProps { allergy: AllergyIntolerance; - isEnteredInError?: boolean; } - function AllergyRow({ allergy, isEnteredInError }: AllergyRowProps) { + function AllergyRow({ allergy }: AllergyRowProps) { + const MAX_NOTE_LENGTH = 15; + const note = allergy.note || ""; + const isLongNote = note.length > MAX_NOTE_LENGTH; + const displayNote = isLongNote + ? `${note.slice(0, MAX_NOTE_LENGTH)}..` + : note; + + useEffect(() => { + console.log( + "Allergy Note:", + allergy.note, + isLongNote, + displayNote, + note.length, + ); + }, [allergy.note, isLongNote, displayNote, note.length]); + return ( - {allergy.code.display} + +
+ {CATEGORY_ICONS[allergy.category ?? ""]} +
+
+ + {allergy.code.display} + - {t(allergy.category)} + {t(allergy.clinical_status)} - {t(allergy.clinical_status)} - - - - {t(allergy.criticality)} {t(allergy.verification_status)} - - - {allergy.created_by.username} + + {note && ( +
+ {displayNote} + {isLongNote && ( + + + + + +

+ {note} +

+
+
+ )} +
+ )} +
+ +
+ + {formatName(allergy.created_by)} +
); @@ -174,15 +231,28 @@ export function AllergyList({ patientId={patientId} encounterId={encounterId} > - +
- - {t("allergen")} - {t("category")} - {t("status")} - {t("criticality")} - {t("verification")} - {t("created_by")} + + + + {t("allergen")} + + + {t("status")} + + + {t("criticality")} + + + {t("verification")} + + + {t("notes")} + + + {t("logged_by")} + @@ -202,25 +272,24 @@ export function AllergyList({ (allergy) => allergy.verification_status === "entered_in_error", ) .map((allergy) => ( - + ))}
{hasEnteredInErrorRecords && !showEnteredInError && ( -
- -
+ <> +
+
+ +
+ )} ); @@ -238,15 +307,15 @@ const AllergyListLayout = ({ children: ReactNode; }) => { return ( - - + + {t("allergies")} {facilityId && encounterId && ( - + {t("edit")} )} diff --git a/src/components/Patient/diagnosis/DiagnosisTable.tsx b/src/components/Patient/diagnosis/DiagnosisTable.tsx index d9134f6d0c8..3f821d5d110 100644 --- a/src/components/Patient/diagnosis/DiagnosisTable.tsx +++ b/src/components/Patient/diagnosis/DiagnosisTable.tsx @@ -1,6 +1,12 @@ import { t } from "i18next"; import { Badge } from "@/components/ui/badge"; +import { Button } from "@/components/ui/button"; +import { + Popover, + PopoverContent, + PopoverTrigger, +} from "@/components/ui/popover"; import { Table, TableBody, @@ -12,101 +18,133 @@ import { import { Avatar } from "@/components/Common/Avatar"; -import { formatName } from "@/Utils/utils"; -import { Diagnosis } from "@/types/emr/diagnosis/diagnosis"; - -export const getStatusBadgeStyle = (status: string) => { - switch (status?.toLowerCase()) { - case "active": - return "bg-green-100 text-green-800 border-green-200"; - case "inactive": - return "bg-gray-100 text-gray-800 border-gray-200"; - case "resolved": - return "bg-blue-100 text-blue-800 border-blue-200"; - case "recurrence": - return "bg-orange-100 text-orange-800 border-orange-200"; - default: - return "bg-gray-100 text-gray-800 border-gray-200"; - } -}; +import { + DIAGNOSIS_CLINICAL_STATUS_STYLES, + DIAGNOSIS_VERIFICATION_STATUS_STYLES, + Diagnosis, +} from "@/types/emr/diagnosis/diagnosis"; interface DiagnosisTableProps { diagnoses: Diagnosis[]; - showHeader?: boolean; } -export function DiagnosisTable({ - diagnoses, - showHeader = true, -}: DiagnosisTableProps) { +export function DiagnosisTable({ diagnoses }: DiagnosisTableProps) { return ( - - {showHeader && ( - - - {t("diagnosis")} - {t("status")} - {t("verification")} - {t("onset")} - {t("notes")} - {t("created_by")} - - - )} +
+ + + + {t("diagnosis")} + + + {t("status")} + + + {t("verification")} + + + {t("onset")} + + + {t("notes")} + + + {t("logged_by")} + + + - {diagnoses.map((diagnosis: Diagnosis, index) => { - const isEnteredInError = - diagnosis.verification_status === "entered_in_error"; + {diagnoses.map((diagnosis) => { + const note = diagnosis.note || ""; + const MAX_NOTE_LENGTH = 15; + const isLongNote = note.length > MAX_NOTE_LENGTH; + const displayNote = isLongNote + ? `${note.slice(0, MAX_NOTE_LENGTH)}..` + : note; return ( - <> - - - {diagnosis.code.display} - - - - {t(diagnosis.clinical_status)} - - - - - {t(diagnosis.verification_status)} - - - - {diagnosis.onset?.onset_datetime - ? new Date( - diagnosis.onset.onset_datetime, - ).toLocaleDateString() - : "-"} - - - {diagnosis.note || "-"} - - + + + {diagnosis.code.display} + + + + {t(diagnosis.clinical_status)} + + + + + {t(diagnosis.verification_status)} + + + + {diagnosis.onset?.onset_datetime + ? new Date( + diagnosis.onset.onset_datetime, + ).toLocaleDateString() + : "-"} + + + {note ? ( +
+ + {displayNote} + + {isLongNote && ( + + + + + +

+ {note} +

+
+
+ )} +
+ ) : ( + "-" + )} +
+ +
- {formatName(diagnosis.created_by)} + {diagnosis.created_by.username} - - - +
+
+
); })}
diff --git a/src/components/Patient/diagnosis/list.tsx b/src/components/Patient/diagnosis/list.tsx index 40ae25b3faf..8e7fe9f1c07 100644 --- a/src/components/Patient/diagnosis/list.tsx +++ b/src/components/Patient/diagnosis/list.tsx @@ -1,9 +1,10 @@ import { useQuery } from "@tanstack/react-query"; import { t } from "i18next"; -import { PencilIcon } from "lucide-react"; import { Link } from "raviger"; import { ReactNode, useState } from "react"; +import CareIcon from "@/CAREUI/icons/CareIcon"; + import { Button } from "@/components/ui/button"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Skeleton } from "@/components/ui/skeleton"; @@ -41,7 +42,9 @@ export function DiagnosisList({ patientId={patientId} encounterId={encounterId} > - + + + ); } @@ -91,16 +94,19 @@ export function DiagnosisList({ /> {hasEnteredInErrorRecords && !showEnteredInError && ( -
- -
+ <> +
+
+ +
+ )} ); @@ -118,15 +124,15 @@ const DiagnosisListLayout = ({ children: ReactNode; }) => { return ( - - + + {t("diagnoses")} {facilityId && encounterId && ( - + {t("edit")} )} diff --git a/src/components/Patient/symptoms/SymptomTable.tsx b/src/components/Patient/symptoms/SymptomTable.tsx index b95e518d99d..8fc626918e9 100644 --- a/src/components/Patient/symptoms/SymptomTable.tsx +++ b/src/components/Patient/symptoms/SymptomTable.tsx @@ -1,6 +1,12 @@ import { t } from "i18next"; import { Badge } from "@/components/ui/badge"; +import { Button } from "@/components/ui/button"; +import { + Popover, + PopoverContent, + PopoverTrigger, +} from "@/components/ui/popover"; import { Table, TableBody, @@ -12,107 +18,135 @@ import { import { Avatar } from "@/components/Common/Avatar"; -import { formatName } from "@/Utils/utils"; -import { Symptom } from "@/types/emr/symptom/symptom"; - -export const getStatusBadgeStyle = (status: string) => { - switch (status?.toLowerCase()) { - case "active": - return "bg-green-100 text-green-800 border-green-200"; - case "inactive": - return "bg-gray-100 text-gray-800 border-gray-200"; - case "resolved": - return "bg-blue-100 text-blue-800 border-blue-200"; - case "recurrence": - return "bg-orange-100 text-orange-800 border-orange-200"; - default: - return "bg-gray-100 text-gray-800 border-gray-200"; - } -}; +import { + SYMPTOM_CLINICAL_STATUS_STYLES, + SYMPTOM_SEVERITY_STYLES, + SYMPTOM_VERIFICATION_STATUS_STYLES, + Symptom, +} from "@/types/emr/symptom/symptom"; interface SymptomTableProps { symptoms: Symptom[]; - showHeader?: boolean; } -export function SymptomTable({ - symptoms, - showHeader = true, -}: SymptomTableProps) { +export function SymptomTable({ symptoms }: SymptomTableProps) { return ( -
- {showHeader && ( - - - {t("symptom")} - {t("status")} - {t("severity")} - {t("onset")} - {t("verification")} - {t("notes")} - {t("created_by")} - - - )} +
+ + + + {t("symptom")} + + + {t("severity")} + + + {t("status")} + + + {t("verification")} + + + {t("notes")} + + + {t("logged_by")} + + + - {symptoms.map((symptom: Symptom, index: number) => { - const isEnteredInError = - symptom.verification_status === "entered_in_error"; + {symptoms.map((symptom) => { + const note = symptom.note || ""; + const MAX_NOTE_LENGTH = 15; + const isLongNote = note.length > MAX_NOTE_LENGTH; + const displayNote = isLongNote + ? `${note.slice(0, MAX_NOTE_LENGTH)}..` + : note; return ( - <> - - - {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 || "-"} - - + + + {symptom.code.display} + + + + {t(symptom.severity)} + + + + + {t(symptom.clinical_status)} + + + + + {t(symptom.verification_status)} + + + + {note ? ( +
+ + {displayNote} + + {isLongNote && ( + + + + + +

+ {note} +

+
+
+ )} +
+ ) : ( + "-" + )} +
+ +
- - {formatName(symptom.created_by)} - - - - + {symptom.created_by.username} +
+
+
); })}
diff --git a/src/components/Patient/symptoms/list.tsx b/src/components/Patient/symptoms/list.tsx index d095b851330..e815a229e0d 100644 --- a/src/components/Patient/symptoms/list.tsx +++ b/src/components/Patient/symptoms/list.tsx @@ -1,9 +1,10 @@ import { useQuery } from "@tanstack/react-query"; import { t } from "i18next"; -import { PencilIcon } from "lucide-react"; import { Link } from "raviger"; import { ReactNode, useState } from "react"; +import CareIcon from "@/CAREUI/icons/CareIcon"; + import { Button } from "@/components/ui/button"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Skeleton } from "@/components/ui/skeleton"; @@ -41,7 +42,9 @@ export function SymptomsList({ patientId={patientId} encounterId={encounterId} > - + + + ); } @@ -89,16 +92,19 @@ export function SymptomsList({ /> {hasEnteredInErrorRecords && !showEnteredInError && ( -
- -
+ <> +
+
+ +
+ )} ); @@ -116,15 +122,15 @@ const SymptomListLayout = ({ children: ReactNode; }) => { return ( - - + + {t("symptoms")} - {facilityId && ( + {facilityId && encounterId && ( - + {t("edit")} )} diff --git a/src/components/Questionnaire/QuestionTypes/AllergyQuestion.tsx b/src/components/Questionnaire/QuestionTypes/AllergyQuestion.tsx index 8270ae98c71..94ffe3b7626 100644 --- a/src/components/Questionnaire/QuestionTypes/AllergyQuestion.tsx +++ b/src/components/Questionnaire/QuestionTypes/AllergyQuestion.tsx @@ -8,12 +8,7 @@ import { Pencil2Icon, } from "@radix-ui/react-icons"; import { useQuery } from "@tanstack/react-query"; -import { - BeakerIcon, - CookingPotIcon, - HeartPulseIcon, - LeafIcon, -} from "lucide-react"; +import { t } from "i18next"; import React, { useEffect, useState } from "react"; import { Button } from "@/components/ui/button"; @@ -42,9 +37,11 @@ import { TableRow, } from "@/components/ui/table"; +import { CATEGORY_ICONS } from "@/components/Patient/allergy/list"; import ValueSetSelect from "@/components/Questionnaire/ValueSetSelect"; import query from "@/Utils/request/query"; +import { dateQueryString } from "@/Utils/utils"; import { ALLERGY_VERIFICATION_STATUS, AllergyIntolerance, @@ -88,13 +85,6 @@ const ALLERGY_CATEGORIES: Record = { biologic: "Biologic", }; -const CATEGORY_ICONS: Record = { - food: , - medication: , - environment: , - biologic: , -}; - function convertToAllergyRequest( allergy: AllergyIntolerance, ): AllergyIntoleranceRequest { @@ -105,7 +95,9 @@ function convertToAllergyRequest( verification_status: allergy.verification_status, category: allergy.category, criticality: allergy.criticality, - last_occurrence: allergy.last_occurrence, + last_occurrence: allergy.last_occurrence + ? dateQueryString(new Date(allergy.last_occurrence)) + : undefined, note: allergy.note, encounter: allergy.encounter, }; @@ -211,15 +203,15 @@ export function AllergyQuestion({ - Substance + {t("substance")} - Critical + {t("criticality")} - Status + {t("status")} - Occurrence + {t("occurrence")} @@ -315,7 +307,7 @@ export function AllergyQuestion({ } > - Mark Active + {t("mark_active")} )} {allergy.clinical_status !== "inactive" && ( @@ -327,7 +319,7 @@ export function AllergyQuestion({ } > - Mark Inactive + {t("mark_inactive")} )} {allergy.clinical_status !== "resolved" && ( @@ -339,7 +331,7 @@ export function AllergyQuestion({ } > - Mark Resolved + {t("mark_resolved")} )} @@ -348,7 +340,7 @@ export function AllergyQuestion({ onClick={() => handleRemoveAllergy(index)} > - Remove Allergy + {t("remove_allergy")} @@ -356,7 +348,9 @@ export function AllergyQuestion({
- + @@ -405,7 +401,9 @@ export function AllergyQuestion({
- + - + @@ -528,12 +528,14 @@ const AllergyTableRow = ({ disabled={disabled} > - + - Low - High - Unable to Assess + {t("low")} + {t("high")} + + {t("unable_to_assess")} + @@ -550,7 +552,7 @@ const AllergyTableRow = ({ disabled={disabled} > - + {Object.entries(ALLERGY_VERIFICATION_STATUS).map( @@ -566,7 +568,7 @@ const AllergyTableRow = ({ onUpdate?.({ last_occurrence: e.target.value })} disabled={disabled} className="h-7 text-sm w-[100px] px-1" @@ -587,14 +589,14 @@ const AllergyTableRow = ({ - {showNotes ? "Hide Notes" : "Add Notes"} + {showNotes ? t("hide_notes") : t("add_notes")} {allergy.clinical_status !== "active" && ( onUpdate?.({ clinical_status: "active" })} > - Mark Active + {t("mark_active")} )} {allergy.clinical_status !== "inactive" && ( @@ -602,7 +604,7 @@ const AllergyTableRow = ({ onClick={() => onUpdate?.({ clinical_status: "inactive" })} > - Mark Inactive + {t("mark_inactive")} )} {allergy.clinical_status !== "resolved" && ( @@ -610,7 +612,7 @@ const AllergyTableRow = ({ onClick={() => onUpdate?.({ clinical_status: "resolved" })} > - Mark Resolved + {t("mark_resolved")} )} @@ -619,7 +621,7 @@ const AllergyTableRow = ({ onClick={onRemove} > - Remove Allergy + {t("remove_allergy")} @@ -628,10 +630,10 @@ const AllergyTableRow = ({ {showNotes && ( - + onUpdate?.({ note: e.target.value })} disabled={disabled} diff --git a/src/pages/Encounters/tabs/EncounterUpdatesTab.tsx b/src/pages/Encounters/tabs/EncounterUpdatesTab.tsx index d6f8a1a08bd..5f6eaa151d9 100644 --- a/src/pages/Encounters/tabs/EncounterUpdatesTab.tsx +++ b/src/pages/Encounters/tabs/EncounterUpdatesTab.tsx @@ -1,4 +1,4 @@ -import ObservationsList from "@/components/Facility/ConsultationDetails/ObservationsList"; +import SideOverview from "@/components/Facility/ConsultationDetails/OverviewSideBar"; import QuestionnaireResponsesList from "@/components/Facility/ConsultationDetails/QuestionnaireResponsesList"; import { AllergyList } from "@/components/Patient/allergy/list"; import { DiagnosisList } from "@/components/Patient/diagnosis/list"; @@ -55,8 +55,8 @@ export const EncounterUpdatesTab = ({
{/* Right Column - Observations */} -
- +
+
diff --git a/src/types/emr/allergyIntolerance/allergyIntolerance.ts b/src/types/emr/allergyIntolerance/allergyIntolerance.ts index 6693664b0b5..b788729091b 100644 --- a/src/types/emr/allergyIntolerance/allergyIntolerance.ts +++ b/src/types/emr/allergyIntolerance/allergyIntolerance.ts @@ -51,4 +51,24 @@ export const ALLERGY_VERIFICATION_STATUS = { refuted: "Refuted", presumed: "Presumed", entered_in_error: "Entered in Error", -}; +} as const; + +export const ALLERGY_VERIFICATION_STATUS_STYLES = { + unconfirmed: "bg-yellow-100 text-yellow-800 border-yellow-200", + confirmed: "bg-green-100 text-green-800 border-green-200", + refuted: "bg-red-100 text-red-800 border-red-200", + presumed: "bg-blue-100 text-blue-800 border-blue-200", + entered_in_error: "bg-red-100 text-red-800 border-red-200", +} as const; + +export const ALLERGY_CLINICAL_STATUS_STYLES = { + active: "bg-green-100 text-green-800 border-green-200", + inactive: "bg-gray-100 text-gray-800 border-gray-200", + resolved: "bg-blue-100 text-blue-800 border-blue-200", +} as const; + +export const ALLERGY_CRITICALITY_STYLES = { + low: "bg-blue-100 text-blue-800 border-blue-200", + high: "bg-red-100 text-red-800 border-red-200", + unable_to_assess: "bg-gray-100 text-gray-800 border-gray-200", +} as const; diff --git a/src/types/emr/diagnosis/diagnosis.ts b/src/types/emr/diagnosis/diagnosis.ts index 29f7b250778..c9dc5035a0d 100644 --- a/src/types/emr/diagnosis/diagnosis.ts +++ b/src/types/emr/diagnosis/diagnosis.ts @@ -54,3 +54,21 @@ export interface DiagnosisRequest { note?: string; encounter: string; } + +export const DIAGNOSIS_CLINICAL_STATUS_STYLES = { + active: "bg-green-100 text-green-800 border-green-200", + recurrence: "bg-yellow-100 text-yellow-800 border-yellow-200", + relapse: "bg-red-100 text-red-800 border-red-200", + inactive: "bg-gray-100 text-gray-800 border-gray-200", + remission: "bg-blue-100 text-blue-800 border-blue-200", + resolved: "bg-emerald-100 text-emerald-800 border-emerald-200", +} as const; + +export const DIAGNOSIS_VERIFICATION_STATUS_STYLES = { + unconfirmed: "bg-yellow-100 text-yellow-800 border-yellow-200", + provisional: "bg-orange-100 text-orange-800 border-orange-200", + differential: "bg-purple-100 text-purple-800 border-purple-200", + confirmed: "bg-green-100 text-green-800 border-green-200", + refuted: "bg-red-100 text-red-800 border-red-200", + entered_in_error: "bg-red-100 text-red-800 border-red-200", +} as const; diff --git a/src/types/emr/symptom/symptom.ts b/src/types/emr/symptom/symptom.ts index d064fd320e4..2d3512a5b59 100644 --- a/src/types/emr/symptom/symptom.ts +++ b/src/types/emr/symptom/symptom.ts @@ -59,3 +59,27 @@ export interface SymptomRequest { note?: string; encounter: string; } + +export const SYMPTOM_CLINICAL_STATUS_STYLES = { + active: "bg-green-100 text-green-800 border-green-200", + recurrence: "bg-yellow-100 text-yellow-800 border-yellow-200", + relapse: "bg-red-100 text-red-800 border-red-200", + inactive: "bg-gray-100 text-gray-800 border-gray-200", + remission: "bg-blue-100 text-blue-800 border-blue-200", + resolved: "bg-emerald-100 text-emerald-800 border-emerald-200", +} as const; + +export const SYMPTOM_VERIFICATION_STATUS_STYLES = { + unconfirmed: "bg-yellow-100 text-yellow-800 border-yellow-200", + provisional: "bg-orange-100 text-orange-800 border-orange-200", + differential: "bg-purple-100 text-purple-800 border-purple-200", + confirmed: "bg-green-100 text-green-800 border-green-200", + refuted: "bg-red-100 text-red-800 border-red-200", + entered_in_error: "bg-red-100 text-red-800 border-red-200", +} as const; + +export const SYMPTOM_SEVERITY_STYLES = { + severe: "bg-red-100 text-red-800 border-red-200", + moderate: "bg-yellow-100 text-yellow-800 border-yellow-200", + mild: "bg-blue-100 text-blue-800 border-blue-200", +} as const;