From 6ffedec0a8b8c633dbdd154162a85ea8cc48605a Mon Sep 17 00:00:00 2001 From: Don Xavier <98073418+DonXavierdev@users.noreply.github.com> Date: Wed, 26 Feb 2025 09:21:32 +0530 Subject: [PATCH 01/21] Redesign: Departments page root view (#10411) Co-authored-by: Jacob John Jeevan --- public/locale/en.json | 6 + .../FacilityOrganizationIndex.tsx | 308 +++++++++++++++--- 2 files changed, 277 insertions(+), 37 deletions(-) diff --git a/public/locale/en.json b/public/locale/en.json index f0bd3d9d836..477ee9e4c6f 100644 --- a/public/locale/en.json +++ b/public/locale/en.json @@ -590,8 +590,12 @@ "clear_search": "Clear search", "clear_selection": "Clear selection", "clear_skill": "Clear Skill", + "click_add_department_team": "Click Add Department/Team to create a new department/team.", + "click_manage_create_users": "Click See Details to create or manage users and departments/teams within the corresponding dept/team.", + "click_manage_create_users_mobile": "Click to create or manage users and departments/teams within the corresponding dept/team.", "close": "Close", "close_scanner": "Close Scanner", + "collapse_all": "Collapse All", "collapse_sidebar": "Collapse Sidebar", "combine_files_pdf": "Combine Files To PDF", "comment_added_successfully": "Comment added successfully", @@ -1057,6 +1061,7 @@ "exception_created": "Exception created successfully", "exception_deleted": "Exception deleted", "exceptions": "Exceptions", + "expand_all": "Expand All", "expand_sidebar": "Expand Sidebar", "expected_burn_rate": "Expected Burn Rate", "expiration_date": "Expiration Date", @@ -1143,6 +1148,7 @@ "filter_by": "Filter By", "filter_by_category": "Filter by category", "filter_by_date": "Filter by Date", + "filter_by_department_or_team_name": "Filter by department or team name", "filters": "Filters", "first_name": "First Name", "food": "Food", diff --git a/src/pages/Facility/settings/organizations/FacilityOrganizationIndex.tsx b/src/pages/Facility/settings/organizations/FacilityOrganizationIndex.tsx index da1e2eae284..5907cc1ce13 100644 --- a/src/pages/Facility/settings/organizations/FacilityOrganizationIndex.tsx +++ b/src/pages/Facility/settings/organizations/FacilityOrganizationIndex.tsx @@ -1,6 +1,11 @@ +import { TooltipContent, TooltipTrigger } from "@radix-ui/react-tooltip"; +import { TooltipProvider } from "@radix-ui/react-tooltip"; +import { Tooltip } from "@radix-ui/react-tooltip"; import { useQuery } from "@tanstack/react-query"; import { Link } from "raviger"; +import { useState } from "react"; import { useTranslation } from "react-i18next"; +import { Trans } from "react-i18next"; import CareIcon from "@/CAREUI/icons/CareIcon"; @@ -9,38 +14,45 @@ import { Card, CardContent, CardDescription, - CardFooter, CardHeader, CardTitle, } from "@/components/ui/card"; +import { Input } from "@/components/ui/input"; import { Skeleton } from "@/components/ui/skeleton"; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@/components/ui/table"; import Page from "@/components/Common/Page"; import { CardGridSkeleton } from "@/components/Common/SkeletonLoading"; +import useBreakpoints from "@/hooks/useBreakpoints"; + import routes from "@/Utils/request/api"; import query from "@/Utils/request/query"; -import { FacilityOrganization } from "@/types/facilityOrganization/facilityOrganization"; - -import CreateFacilityOrganizationSheet from "./components/CreateFacilityOrganizationSheet"; +import CreateFacilityOrganizationSheet from "@/pages/Facility/settings/organizations/components/CreateFacilityOrganizationSheet"; export default function FacilityOrganizationIndex({ facilityId, }: { facilityId: string; }) { + const [expandedRows, setExpandedRows] = useState>({}); const { t } = useTranslation(); const { data, isLoading } = useQuery({ queryKey: ["facilityOrganization", "list", facilityId], - queryFn: query(routes.facilityOrganization.list, { + queryFn: query.paginated(routes.facilityOrganization.list, { pathParams: { facilityId }, - queryParams: { - parent: "", - }, }), enabled: !!facilityId, }); - + const tableData = data?.results || []; + const isMobile = useBreakpoints({ default: true, sm: false }); if (isLoading) { return (
@@ -53,7 +65,7 @@ export default function FacilityOrganizationIndex({ ); } - if (!data?.results?.length) { + if (!tableData?.length) { return (
@@ -80,38 +92,260 @@ export default function FacilityOrganizationIndex({ ); } + const toggleRow = (id: string) => { + const newExpandedRows = { ...expandedRows }; + newExpandedRows[id] = !newExpandedRows[id]; + const children = getChildren(id); + children.forEach((child) => { + if (!child.has_children) { + newExpandedRows[child.id] = !newExpandedRows[child.id]; + } + }); + setExpandedRows(newExpandedRows); + }; + const getChildren = (parentId: string) => { + return tableData.filter((org) => org.parent?.id === parentId); + }; + const OrganizationRow = ({ + org, + expandedRows, + toggleRow, + getChildren, + indent, + }: { + org: { + id: string; + name: string; + parent?: { id: string }; + org_type: string; + }; + expandedRows: Record; + toggleRow: (id: string) => void; + getChildren: (parentId: string) => { + id: string; + name: string; + parent?: { id: string }; + org_type: string; + }[]; + indent: number; + }) => { + const children = getChildren(org.id); + const isTopLevel = !org.parent || Object.keys(org.parent).length === 0; + + const toggleAllChildren = () => { + setExpandedRows((prevExpandedRows) => { + const newExpandedRows = { ...prevExpandedRows }; + const toggleChildren = (parentId: string, expand: boolean) => { + getChildren(parentId).forEach((child) => { + newExpandedRows[child.id] = expand; + toggleChildren(child.id, expand); + }); + }; + const shouldExpand = !children.every( + (child) => prevExpandedRows[child.id], + ); + newExpandedRows[org.id] = shouldExpand; + toggleChildren(org.id, shouldExpand); + return newExpandedRows; + }); + }; + const allExpanded = children.every((child) => expandedRows[child.id]); + return ( + <> + + +
+ {isTopLevel || children.length > 0 ? ( + + ) : ( + + )} + + {org.name} +
+
+ {isTopLevel && children.length > 0 && ( +
+ + + + + + + {t(allExpanded ? "collapse_all" : "expand_all")} + + + +
+ )} +
+ + + + + + {t("see_details")} + + +
+
+
+ {!isMobile && ( + + {t(`facility_organization_type__${org.org_type}`)} + + )} +
+ {expandedRows[org.id] && + children.map((child) => ( + + ))} + + ); + }; return ( -
- +

{t("departments")}

+
+
+ +
+
+ +
-
- {data.results.map((org: FacilityOrganization) => ( - - -
- - {org.name} - - {org.org_type} -
-
- - - - -
- ))} +
+
+ +
+
+
+ , + }} + /> +
+
+ , + CareIcon: ( + + ), + }} + /> +
+
+ + ), + }} + /> +
+
+ + + + + {t("name")} + + {!isMobile && ( + + {t("category")} + + )} + + + + {tableData + .filter( + (org) => !org.parent || Object.keys(org.parent).length === 0, + ) // Parent rows only + .map((parent) => ( + + ))} + +
); } From fde54b3cbbdad4dd584a2939851700098cc98570 Mon Sep 17 00:00:00 2001 From: Abhimanyu Rajeesh <63541653+abhimanyurajeesh@users.noreply.github.com> Date: Wed, 26 Feb 2025 14:14:54 +0530 Subject: [PATCH 02/21] Fix: Add a new list view to locations in the facility settings (#10672) Co-authored-by: Jacob John Jeevan --- public/locale/en.json | 12 +- .../settings/locations/LocationList.tsx | 345 ++++++++++++++---- .../locations/components/LocationListView.tsx | 248 +++++++++++++ 3 files changed, 526 insertions(+), 79 deletions(-) create mode 100644 src/pages/Facility/settings/locations/components/LocationListView.tsx diff --git a/public/locale/en.json b/public/locale/en.json index 477ee9e4c6f..086561744a7 100644 --- a/public/locale/en.json +++ b/public/locale/en.json @@ -590,9 +590,14 @@ "clear_search": "Clear search", "clear_selection": "Clear selection", "clear_skill": "Clear Skill", + "click": "Click", "click_add_department_team": "Click Add Department/Team to create a new department/team.", + "click_add_main_location": "Click Add Location to add a main location.", "click_manage_create_users": "Click See Details to create or manage users and departments/teams within the corresponding dept/team.", "click_manage_create_users_mobile": "Click to create or manage users and departments/teams within the corresponding dept/team.", + "click_manage_sub_locations": "Click See Details to manage sub-locations.", + "click_manage_sub_locations_mobile": "Click to edit and to manage sub-locations.", + "click_on": "Click on", "close": "Close", "close_scanner": "Close Scanner", "collapse_all": "Collapse All", @@ -1149,6 +1154,7 @@ "filter_by_category": "Filter by category", "filter_by_date": "Filter by Date", "filter_by_department_or_team_name": "Filter by department or team name", + "filter_by_locations": "Filter by Locations", "filters": "Filters", "first_name": "First Name", "food": "Food", @@ -1210,6 +1216,7 @@ "hospital_identifier": "Hospital Identifier", "hospitalisation_details": "Hospitalization Details", "hospitalization_details": "Hospitalization Details", + "hover_focus_reveal": "Hover or focus to reveal", "hubs": "Hub Facilities", "i_declare": "I hereby declare that:", "icd11_as_recommended": "As per ICD-11 recommended by WHO", @@ -1408,6 +1415,7 @@ "manufacture_date_cannot_be_in_future": "Manufacture date cannot be in future", "manufactured": "Manufactured", "manufacturer": "Manufacturer", + "map": "Map", "map_acronym": "M.A.P.", "mark_active": "Mark Active", "mark_all_as_read": "Mark all as Read", @@ -2013,8 +2021,8 @@ "save": "Save", "save_and_continue": "Save and Continue", "save_investigation": "Save Investigation", + "save_valueset": "Save ValueSet", "saving": "Saving...", - "retired":"Retired", "scan_asset_qr": "Scan Asset QR!", "schedule": "Schedule", "schedule_an_appointment_or_create_a_new_encounter": "Schedule an appointment or create a new encounter", @@ -2291,6 +2299,7 @@ "titrate_dosage": "Titrate Dosage", "to": "to", "to_be_conducted": "To be conducted", + "to_edit": "to edit", "to_proceed_with_registration": "To proceed with registration, please create a new patient.", "to_view_available_slots_select_resource_and_date": "To view available slots, select a preferred resource and date.", "today": "Today", @@ -2468,7 +2477,6 @@ "valid_year_of_birth": "Please enter a valid year of birth (YYYY)", "value": "Value", "value_set": "Value Set", - "save_valueset":"Save ValueSet", "valuesets": "Valuesets", "vehicle_preference": "Vehicle preference", "vendor_name": "Vendor Name", diff --git a/src/pages/Facility/settings/locations/LocationList.tsx b/src/pages/Facility/settings/locations/LocationList.tsx index 4cb04e934ee..6d1187bec57 100644 --- a/src/pages/Facility/settings/locations/LocationList.tsx +++ b/src/pages/Facility/settings/locations/LocationList.tsx @@ -1,49 +1,131 @@ import { useQuery } from "@tanstack/react-query"; -import { useState } from "react"; -import { useTranslation } from "react-i18next"; +import { PenLine } from "lucide-react"; +import { useCallback, useEffect, useMemo, useState } from "react"; +import { Trans, useTranslation } from "react-i18next"; import CareIcon from "@/CAREUI/icons/CareIcon"; import { Button } from "@/components/ui/button"; -import { Card, CardContent } from "@/components/ui/card"; import { Input } from "@/components/ui/input"; +import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs"; -import Pagination from "@/components/Common/Pagination"; -import { CardGridSkeleton } from "@/components/Common/SkeletonLoading"; +import Page from "@/components/Common/Page"; import query from "@/Utils/request/query"; +import { useView } from "@/Utils/useView"; import { LocationList as LocationListType } from "@/types/location/location"; import locationApi from "@/types/location/locationApi"; import LocationSheet from "./LocationSheet"; -import { LocationCard } from "./components/LocationCard"; +import { LocationListView } from "./components/LocationListView"; interface Props { facilityId: string; } +function createSearchMatcher(query: string) { + const normalizedQuery = query.toLowerCase(); + return (name: string) => name.toLowerCase().includes(normalizedQuery); +} + +function buildLocationHierarchy(locations: LocationListType[]) { + const childrenMap = new Map(); + const topLevelLocations: LocationListType[] = []; + + locations.forEach((location) => { + if (!location.parent || Object.keys(location.parent).length === 0) { + topLevelLocations.push(location); + } else { + const parentId = location.parent.id; + if (!childrenMap.has(parentId)) { + childrenMap.set(parentId, []); + } + childrenMap.get(parentId)?.push(location); + } + }); + + return { childrenMap, topLevelLocations }; +} + export default function LocationList({ facilityId }: Props) { const { t } = useTranslation(); - const [page, setPage] = useState(1); const [searchQuery, setSearchQuery] = useState(""); const [selectedLocation, setSelectedLocation] = useState(null); const [isSheetOpen, setIsSheetOpen] = useState(false); - const limit = 12; + const [activeTab, setActiveTab] = useView("locations", "list"); + const [expandedRows, setExpandedRows] = useState>({}); + const [{ childrenMap, topLevelLocations }, setLocationHierarchy] = useState<{ + childrenMap: Map; + topLevelLocations: LocationListType[]; + }>({ childrenMap: new Map(), topLevelLocations: [] }); const { data, isLoading } = useQuery({ - queryKey: ["locations", facilityId, page, limit, searchQuery], - queryFn: query.debounced(locationApi.list, { + queryKey: ["locations", facilityId], + queryFn: query.paginated(locationApi.list, { pathParams: { facility_id: facilityId }, - queryParams: { - parent: "", - offset: (page - 1) * limit, - limit, - name: searchQuery || undefined, - }, + queryParams: {}, }), + enabled: !!facilityId, }); + useEffect(() => { + setLocationHierarchy(buildLocationHierarchy(data?.results || [])); + }, [data?.results]); + + const filteredData = useMemo(() => { + if (!searchQuery) return data?.results || []; + + const matchesSearch = createSearchMatcher(searchQuery); + + const hasMatchingDescendant = (locationId: string): boolean => { + const children = childrenMap.get(locationId) || []; + return children.some( + (child: LocationListType) => + matchesSearch(child.name) || hasMatchingDescendant(child.id), + ); + }; + + return data?.results?.filter( + (location) => + matchesSearch(location.name) || hasMatchingDescendant(location.id), + ); + }, [data?.results, searchQuery, childrenMap]); + + const matchesSearch = useMemo( + () => createSearchMatcher(searchQuery), + [searchQuery], + ); + + const hasMatchingChildren = useCallback( + (parentId: string): boolean => { + const children = childrenMap.get(parentId) || []; + return children.some( + (child: LocationListType) => + matchesSearch(child.name) || hasMatchingChildren(child.id), + ); + }, + [childrenMap, matchesSearch], + ); + + const getChildren = (parentId: string): LocationListType[] => { + const children = childrenMap.get(parentId) || []; + if (!searchQuery) return children; + + return children.filter( + (loc: LocationListType) => + matchesSearch(loc.name) || hasMatchingChildren(loc.id), + ); + }; + + const filteredTopLevelLocations = useMemo(() => { + if (!searchQuery) return topLevelLocations; + return topLevelLocations.filter( + (loc: LocationListType) => + matchesSearch(loc.name) || hasMatchingChildren(loc.id), + ); + }, [topLevelLocations, searchQuery, matchesSearch, hasMatchingChildren]); + const handleAddLocation = () => { setSelectedLocation(null); setIsSheetOpen(true); @@ -59,72 +141,181 @@ export default function LocationList({ facilityId }: Props) { setSelectedLocation(null); }; + const toggleRow = (id: string) => { + const newExpandedRows = { ...expandedRows }; + newExpandedRows[id] = !newExpandedRows[id]; + const children = getChildren(id); + children.forEach((child) => { + if (!child.has_children) { + newExpandedRows[child.id] = !newExpandedRows[child.id]; + } + }); + setExpandedRows(newExpandedRows); + }; + + useEffect(() => { + if (!searchQuery) { + setExpandedRows({}); + return; + } + + const allLocations = data?.results || []; + const matchesSearch = createSearchMatcher(searchQuery); + + const hasMatchingDescendant = (locationId: string): boolean => { + const children = allLocations.filter( + (loc) => loc.parent?.id === locationId, + ); + return children.some( + (child: LocationListType) => + matchesSearch(child.name) || hasMatchingDescendant(child.id), + ); + }; + + const newExpandedRows: Record = {}; + allLocations.forEach((location) => { + if (matchesSearch(location.name) || hasMatchingDescendant(location.id)) { + let currentLoc = location; + while (currentLoc.parent?.id) { + newExpandedRows[currentLoc.parent.id] = true; + const parentLoc = allLocations.find( + (loc) => loc.id === currentLoc.parent?.id, + ); + if (!parentLoc) { + break; + } + currentLoc = parentLoc; + } + } + }); + + setExpandedRows(newExpandedRows); + }, [searchQuery, data?.results]); + return ( -
-
-
-

{t("locations")}

- -
-
- { - setSearchQuery(e.target.value); - setPage(1); - }} - className="w-full" - /> -
-
+ +
+

{t("locations")}

+
+
+
+ setActiveTab(value as "list" | "map")} + > + + +
+ + {t("list")} +
+
+ {/* Map view will be added later + +
+ + {t("map")} +
+
+ */} +
+
+
- {isLoading ? ( -
- -
- ) : ( -
-
- {data?.results?.length ? ( - data.results.map((location: LocationListType) => ( - - )) - ) : ( - - - {searchQuery - ? t("no_locations_found") - : t("no_locations_available")} - - - )} -
- {data && data.count > limit && ( -
- setPage(page)} - defaultPerPage={limit} - cPage={page} +
+ { + setSearchQuery(e.target.value); + }} + className="w-full text-xs lg:text-sm" /> + +
+
+ + {activeTab === "list" && ( +
+
+
+ +
+
+
+ , + }} + /> +
+ {/* Desktop view text */} +
+ + ), + strong: , + }} + /> +
+ {/* Mobile and Tablet view text */} +
+ + ), + PenLine: , + }} + /> +
+
+
)} + + {/* Map view will be added later, for now always show list view */} + + +
- )} - -
+
+ ); } diff --git a/src/pages/Facility/settings/locations/components/LocationListView.tsx b/src/pages/Facility/settings/locations/components/LocationListView.tsx new file mode 100644 index 00000000000..30a30b22699 --- /dev/null +++ b/src/pages/Facility/settings/locations/components/LocationListView.tsx @@ -0,0 +1,248 @@ +import { PenLine } from "lucide-react"; +import { Link } from "raviger"; +import { useTranslation } from "react-i18next"; + +import CareIcon from "@/CAREUI/icons/CareIcon"; + +import { Button } from "@/components/ui/button"; +import { Card, CardContent } from "@/components/ui/card"; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@/components/ui/table"; + +import { TableSkeleton } from "@/components/Common/SkeletonLoading"; + +import useBreakpoints from "@/hooks/useBreakpoints"; + +import { LocationList as LocationListType } from "@/types/location/location"; + +interface LocationRowProps { + location: LocationListType; + expandedRows: Record; + toggleRow: (id: string) => void; + getChildren: (parentId: string) => LocationListType[]; + indent: number; + onEdit: (location: LocationListType) => void; + setExpandedRows: React.Dispatch< + React.SetStateAction> + >; + displayExpandAll?: boolean; +} + +function LocationRow({ + location, + expandedRows, + toggleRow, + getChildren, + indent, + onEdit, + setExpandedRows, + displayExpandAll = true, +}: LocationRowProps) { + const { t } = useTranslation(); + const isMobile = useBreakpoints({ default: true, sm: false }); + const children = getChildren(location.id); + const isTopLevel = + !location.parent || Object.keys(location.parent).length === 0; + const isExpanded = expandedRows[location.id]; + + const toggleAllChildren = () => { + setExpandedRows((prevExpandedRows) => { + const newExpandedRows = { ...prevExpandedRows }; + const toggleChildren = (parentId: string, expand: boolean) => { + getChildren(parentId).forEach((child) => { + newExpandedRows[child.id] = expand; + toggleChildren(child.id, expand); + }); + }; + const shouldExpand = !children.every( + (child) => prevExpandedRows[child.id], + ); + newExpandedRows[location.id] = shouldExpand; + toggleChildren(location.id, shouldExpand); + return newExpandedRows; + }); + }; + + const allExpanded = children.every((child) => expandedRows[child.id]); + + return ( + <> + + +
+ {isTopLevel || children.length > 0 ? ( + + ) : location.parent ? ( + + ) : ( +
+ )} + {location.name} +
+ {isTopLevel && ( +
+
+ {children.length > 0 && displayExpandAll && ( + + )} +
+ +
+ + + +
+
+ )} + + + {t(`location_form__${location.form}`)} + + + {isExpanded && + children.map((child) => ( + + ))} + + ); +} + +interface LocationListViewProps { + isLoading: boolean; + tableData: LocationListType[]; + searchQuery: string; + filteredTopLevelLocations: LocationListType[]; + expandedRows: Record; + toggleRow: (id: string) => void; + getChildren: (parentId: string) => LocationListType[]; + handleEditLocation: (location: LocationListType) => void; + setExpandedRows: React.Dispatch< + React.SetStateAction> + >; +} + +export function LocationListView({ + isLoading, + tableData, + searchQuery, + filteredTopLevelLocations, + expandedRows, + toggleRow, + getChildren, + handleEditLocation, + setExpandedRows, +}: LocationListViewProps) { + const { t } = useTranslation(); + + if (isLoading) { + return ; + } + + if (!tableData?.length) { + return ( + + + {searchQuery ? t("no_locations_found") : t("no_locations_available")} + + + ); + } + + return ( +
+ + + + + {t("name")} + + + {t("location_form")} + + + + + {filteredTopLevelLocations.map((location) => ( + + ))} + +
+
+ ); +} From c09795703be24e98e131298cf621a2a7d3358a2a Mon Sep 17 00:00:00 2001 From: Sulochan Khadka <122200551+Sulochan-khadka@users.noreply.github.com> Date: Wed, 26 Feb 2025 14:40:56 +0530 Subject: [PATCH 03/21] Redirect on updating questionnaire (#10796) --- src/components/Questionnaire/QuestionnaireEditor.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/components/Questionnaire/QuestionnaireEditor.tsx b/src/components/Questionnaire/QuestionnaireEditor.tsx index 9156e109ac5..daf1c189f70 100644 --- a/src/components/Questionnaire/QuestionnaireEditor.tsx +++ b/src/components/Questionnaire/QuestionnaireEditor.tsx @@ -699,9 +699,10 @@ export default function QuestionnaireEditor({ id }: QuestionnaireEditorProps) { mutationFn: mutate(questionnaireApi.update, { pathParams: { id: id! }, }), - onSuccess: () => { + onSuccess: (data: QuestionnaireDetail) => { toast.success("Questionnaire updated successfully"); queryClient.invalidateQueries({ queryKey: ["questionnaireDetail", id] }); + navigate(`/admin/questionnaire/${data.slug}`); }, onError: (_error) => { toast.error("Failed to update questionnaire"); From b40c931ffe35e432b9cdd62843fff6c0820a7ecf Mon Sep 17 00:00:00 2001 From: Aditya Jindal Date: Thu, 27 Feb 2025 11:08:00 +0530 Subject: [PATCH 04/21] Treatment Summary UI Enhancement (#10563) Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- public/locale/en.json | 2 + src/Routers/routes/ConsultationRoutes.tsx | 8 +- src/components/Common/PrintTable.tsx | 66 ++++ .../QuestionnaireResponsesList.tsx | 28 +- .../AdministrationTab.tsx | 203 +++++----- src/components/Medicine/MedicationsTable.tsx | 39 +- .../Patient/MedicationStatementList.tsx | 119 +++--- src/components/Patient/TreatmentSummary.tsx | 372 +++++++++++++++--- src/components/Patient/allergy/list.tsx | 63 +-- .../Patient/diagnosis/DiagnosisTable.tsx | 43 +- src/components/Patient/diagnosis/list.tsx | 85 ++-- .../Patient/symptoms/SymptomTable.tsx | 43 +- src/components/Patient/symptoms/list.tsx | 25 +- 13 files changed, 677 insertions(+), 419 deletions(-) create mode 100644 src/components/Common/PrintTable.tsx diff --git a/public/locale/en.json b/public/locale/en.json index 086561744a7..71ad059d7da 100644 --- a/public/locale/en.json +++ b/public/locale/en.json @@ -1510,6 +1510,7 @@ "next_sessions": "Next Sessions", "next_week_short": "Next wk", "no": "No", + "no_active_medication_recorded": "No Active Medication Recorded", "no_address_provided": "No address provided", "no_allergies_recorded": "No allergies recorded", "no_appointments": "No appointments found", @@ -1551,6 +1552,7 @@ "no_log_update_delta": "No changes since previous log update", "no_log_updates": "No log updates found", "no_medical_history_available": "No Medical History Available", + "no_medication_recorded": "No Medication Recorded", "no_medications": "No Medications", "no_medications_found_for_this_encounter": "No medications found for this encounter.", "no_medications_to_administer": "No medications to administer", diff --git a/src/Routers/routes/ConsultationRoutes.tsx b/src/Routers/routes/ConsultationRoutes.tsx index 119ac148aa9..080b90a0a54 100644 --- a/src/Routers/routes/ConsultationRoutes.tsx +++ b/src/Routers/routes/ConsultationRoutes.tsx @@ -16,8 +16,12 @@ const consultationRoutes: AppRoutes = { /> ), "/facility/:facilityId/patient/:patientId/encounter/:encounterId/treatment_summary": - ({ facilityId, encounterId }) => ( - + ({ facilityId, encounterId, patientId }) => ( + ), "/facility/:facilityId/patient/:patientId/encounter/:encounterId/questionnaire": ({ facilityId, encounterId, patientId }) => ( diff --git a/src/components/Common/PrintTable.tsx b/src/components/Common/PrintTable.tsx new file mode 100644 index 00000000000..7040e1e782c --- /dev/null +++ b/src/components/Common/PrintTable.tsx @@ -0,0 +1,66 @@ +import { t } from "i18next"; + +import { cn } from "@/lib/utils"; + +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@/components/ui/table"; + +type HeaderRow = { + key: string; + width?: number; +}; + +type TableRowType = Record; +interface GenericTableProps { + headers: HeaderRow[]; + rows: TableRowType[] | undefined; +} + +export default function PrintTable({ headers, rows }: GenericTableProps) { + return ( +
+ + + + {headers.map(({ key, width }, index) => ( + + {t(key)} + + ))} + + + + {!!rows && + rows.map((row, index) => ( + + {headers.map(({ key }) => ( + + {row[key] || "-"} + + ))} + + ))} + +
+
+ ); +} diff --git a/src/components/Facility/ConsultationDetails/QuestionnaireResponsesList.tsx b/src/components/Facility/ConsultationDetails/QuestionnaireResponsesList.tsx index 2514c7bbdfe..c6fc5667222 100644 --- a/src/components/Facility/ConsultationDetails/QuestionnaireResponsesList.tsx +++ b/src/components/Facility/ConsultationDetails/QuestionnaireResponsesList.tsx @@ -197,12 +197,23 @@ function StructuredResponseBadge({ ); } -function ResponseCard({ item }: { item: QuestionnaireResponse }) { +function ResponseCard({ + item, + isPrintPreview, +}: { + item: QuestionnaireResponse; + isPrintPreview?: boolean; +}) { const isStructured = !item.questionnaire; const structuredType = Object.keys(item.structured_responses || {})[0]; return ( - +
@@ -317,7 +328,12 @@ export default function QuestionnaireResponsesList({ ) : (
{questionnarieResponses?.results?.length === 0 ? ( - +
{t("no_questionnaire_responses")}
@@ -327,7 +343,11 @@ export default function QuestionnaireResponsesList({ {questionnarieResponses?.results?.map( (item: QuestionnaireResponse) => (
  • - +
  • ), )} diff --git a/src/components/Medicine/MedicationAdministration/AdministrationTab.tsx b/src/components/Medicine/MedicationAdministration/AdministrationTab.tsx index 93d8c430717..b70ec7e45e8 100644 --- a/src/components/Medicine/MedicationAdministration/AdministrationTab.tsx +++ b/src/components/Medicine/MedicationAdministration/AdministrationTab.tsx @@ -8,7 +8,7 @@ import React, { useCallback, useMemo, useState } from "react"; import CareIcon from "@/CAREUI/icons/CareIcon"; import { Button } from "@/components/ui/button"; -import { Card } from "@/components/ui/card"; +import { Card, CardContent } from "@/components/ui/card"; import { Input } from "@/components/ui/input"; import { Popover, @@ -647,113 +647,124 @@ export const AdministrationTab: React.FC = ({ content = ; } else { content = ( - - -
    - {/* Top row without vertical borders */} -
    -
    -
    - {lastModifiedDate && ( -
    - {t("last_modified")}{" "} - {formatDistanceToNow(lastModifiedDate)} {t("ago")} -
    - )} + <> + {!filteredMedications.length && ( + +

    + {t("no_active_medication_recorded")} +

    +
    + )} + + +
    + {/* Top row without vertical borders */} +
    +
    +
    + {lastModifiedDate && ( +
    + {t("last_modified")}{" "} + {formatDistanceToNow(lastModifiedDate)} {t("ago")} +
    + )} +
    +
    + +
    -
    + {visibleSlots.map((slot) => ( + + ))} +
    - {visibleSlots.map((slot) => ( - - ))} -
    - -
    -
    - {/* Main content with borders */} -
    - {/* Headers */} -
    - {t("medicine")}: -
    - {visibleSlots.map((slot, i) => ( -
    - {i === endSlotIndex && - slot.date.getTime() === currentDate.getTime() && ( -
    -
    -
    - )} - {slot.label} + {/* Main content with borders */} +
    + {/* Headers */} +
    + {t("medicine")}:
    - ))} -
    - - {/* Medication rows */} - {filteredMedications?.map((medication) => ( - - ))} + {visibleSlots.map((slot, i) => ( +
    + {i === endSlotIndex && + slot.date.getTime() === currentDate.getTime() && ( +
    +
    +
    + )} + {slot.label} +
    + ))} +
    + + {/* Medication rows */} + {filteredMedications?.map((medication) => ( + + ))} +
    -
    - {stoppedMedications?.results?.length > 0 && !searchQuery.trim() && ( -
    setShowStopped(!showStopped)} - > - - - {showStopped ? t("hide") : t("show")}{" "} - {`${stoppedMedications?.results?.length} ${t("stopped")}`}{" "} - {t("prescriptions")} - -
    - )} - - - + {stoppedMedications?.results?.length > 0 && !searchQuery.trim() && ( +
    setShowStopped(!showStopped)} + > + + + {showStopped ? t("hide") : t("show")}{" "} + {`${stoppedMedications?.results?.length} ${t("stopped")}`}{" "} + {t("prescriptions")} + +
    + )} + + + + ); } diff --git a/src/components/Medicine/MedicationsTable.tsx b/src/components/Medicine/MedicationsTable.tsx index e347d2a9ef7..d78c736fd28 100644 --- a/src/components/Medicine/MedicationsTable.tsx +++ b/src/components/Medicine/MedicationsTable.tsx @@ -1,7 +1,6 @@ -import { useQuery } from "@tanstack/react-query"; import { useTranslation } from "react-i18next"; -import { Skeleton } from "@/components/ui/skeleton"; +import { CardContent } from "@/components/ui/card"; import { Table, TableBody, @@ -13,14 +12,12 @@ import { import { reverseFrequencyOption } from "@/components/Questionnaire/QuestionTypes/MedicationRequestQuestion"; -import query from "@/Utils/request/query"; import { INACTIVE_MEDICATION_STATUSES, MEDICATION_REQUEST_TIMING_OPTIONS, MedicationRequestDosageInstruction, MedicationRequestRead, } from "@/types/emr/medicationRequest"; -import medicationRequestApi from "@/types/emr/medicationRequest/medicationRequestApi"; import { formatDosage, formatSig } from "./utils"; @@ -37,38 +34,22 @@ export function getFrequencyDisplay( } interface MedicationsTableProps { - patientId?: string; - encounterId?: string; - medications?: MedicationRequestRead[]; + medications: MedicationRequestRead[]; } -export const MedicationsTable = ({ - medications, - patientId, - encounterId, -}: MedicationsTableProps) => { +export const MedicationsTable = ({ medications }: MedicationsTableProps) => { const { t } = useTranslation(); - const { data: allMedications, isLoading } = useQuery({ - queryKey: ["medication_requests", patientId, encounterId], - queryFn: query(medicationRequestApi.list, { - pathParams: { patientId }, - queryParams: { encounter: encounterId, limit: 50, offset: 0 }, - }), - enabled: !medications && !!patientId, - }); - - if (isLoading) { + if (!medications.length) { return ( -
    - -
    + +

    + {t("no_active_medication_recorded")} +

    +
    ); } - const displayedMedications = !medications - ? (allMedications?.results ?? []) - : medications; return (
    @@ -82,7 +63,7 @@ export const MedicationsTable = ({ - {displayedMedications?.map((medication) => { + {medications.map((medication) => { const instruction = medication.dosage_instruction[0]; const frequency = getFrequencyDisplay(instruction?.timing); const dosage = formatDosage(instruction); diff --git a/src/components/Patient/MedicationStatementList.tsx b/src/components/Patient/MedicationStatementList.tsx index 112c42414a4..606c00962d7 100644 --- a/src/components/Patient/MedicationStatementList.tsx +++ b/src/components/Patient/MedicationStatementList.tsx @@ -1,4 +1,5 @@ import { useQuery } from "@tanstack/react-query"; +import { t } from "i18next"; import { useState } from "react"; import { useTranslation } from "react-i18next"; @@ -35,20 +36,14 @@ import medicationStatementApi from "@/types/emr/medicationStatement/medicationSt interface MedicationStatementListProps { patientId: string; className?: string; - isPrintPreview?: boolean; } interface MedicationRowProps { statement: MedicationStatementRead; isEnteredInError?: boolean; - isPrintPreview?: boolean; } -function MedicationRow({ - statement, - isEnteredInError, - isPrintPreview = false, -}: MedicationRowProps) { +function MedicationRow({ statement, isEnteredInError }: MedicationRowProps) { const { t } = useTranslation(); return ( @@ -80,26 +75,22 @@ function MedicationRow({ {statement.note ? (
    - {isPrintPreview ? ( - {statement.note} - ) : ( - - - - - -

    - {statement.note} -

    -
    -
    - )} + + + + + +

    + {statement.note} +

    +
    +
    ) : ( "-" @@ -121,11 +112,10 @@ function MedicationRow({ export function MedicationStatementList({ patientId, - className, - isPrintPreview = false, + className = "", }: MedicationStatementListProps) { const { t } = useTranslation(); - const [showEnteredInError, setShowEnteredInError] = useState(isPrintPreview); + const [showEnteredInError, setShowEnteredInError] = useState(false); const { data: medications, isLoading } = useQuery({ queryKey: ["medication_statements", patientId], @@ -136,16 +126,9 @@ export function MedicationStatementList({ if (isLoading) { return ( - - - {t("ongoing_medications")} - - - - - + + + ); } @@ -160,31 +143,18 @@ export function MedicationStatementList({ if (!filteredMedications?.length) { return ( - - - {t("ongoing_medications")} - - -

    {t("no_ongoing_medications")}

    -
    -
    + +

    {t("no_ongoing_medications")}

    +
    ); } return ( - - - - {t("ongoing_medications")} ({filteredMedications.length}) - - - + + <>
    @@ -226,7 +196,6 @@ export function MedicationStatementList({ key={statement.id} statement={statement} isEnteredInError={statement.status === "entered_in_error"} - isPrintPreview={isPrintPreview} /> ))} @@ -246,7 +215,29 @@ export function MedicationStatementList({ )} - - + + ); } + +const MedicationStatementListLayout = ({ + children, + className, + medicationsCount, +}: { + children: React.ReactNode; + className?: string; + medicationsCount?: number | undefined; +}) => { + return ( + + + + {t("ongoing_medications")}{" "} + {medicationsCount ? `(${medicationsCount})` : ""} + + + {children} + + ); +}; diff --git a/src/components/Patient/TreatmentSummary.tsx b/src/components/Patient/TreatmentSummary.tsx index ad87eee91a6..862d7dca5b8 100644 --- a/src/components/Patient/TreatmentSummary.tsx +++ b/src/components/Patient/TreatmentSummary.tsx @@ -2,38 +2,133 @@ import careConfig from "@careConfig"; import { useQuery } from "@tanstack/react-query"; import { format } from "date-fns"; import { t } from "i18next"; +import { Loader } from "lucide-react"; import PrintPreview from "@/CAREUI/misc/PrintPreview"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; + +import Loading from "@/components/Common/Loading"; +import PrintTable from "@/components/Common/PrintTable"; import QuestionnaireResponsesList from "@/components/Facility/ConsultationDetails/QuestionnaireResponsesList"; -import { MedicationsTable } from "@/components/Medicine/MedicationsTable"; -import { AllergyList } from "@/components/Patient/allergy/list"; -import { DiagnosisList } from "@/components/Patient/diagnosis/list"; -import { SymptomsList } from "@/components/Patient/symptoms/list"; +import { getFrequencyDisplay } from "@/components/Medicine/MedicationsTable"; +import { formatDosage, formatSig } from "@/components/Medicine/utils"; import api from "@/Utils/request/api"; import query from "@/Utils/request/query"; -import { formatName, formatPatientAge } from "@/Utils/utils"; - -import { MedicationStatementList } from "./MedicationStatementList"; +import { formatDateTime, formatName, formatPatientAge } from "@/Utils/utils"; +import allergyIntoleranceApi from "@/types/emr/allergyIntolerance/allergyIntoleranceApi"; +import diagnosisApi from "@/types/emr/diagnosis/diagnosisApi"; +import { completedEncounterStatus } from "@/types/emr/encounter"; +import medicationRequestApi from "@/types/emr/medicationRequest/medicationRequestApi"; +import medicationStatementApi from "@/types/emr/medicationStatement/medicationStatementApi"; +import symptomApi from "@/types/emr/symptom/symptomApi"; interface TreatmentSummaryProps { facilityId: string; encounterId: string; + + patientId: string; } +const SectionLayout = ({ + children, + title, +}: { + title: string; + children: React.ReactNode; +}) => { + return ( + + + {title} + + {children} + + ); +}; + +const EmptyState = ({ message }: { message: string }) => { + return ( + +

    {message}

    +
    + ); +}; export default function TreatmentSummary({ facilityId, encounterId, + patientId, }: TreatmentSummaryProps) { - const { data: encounter } = useQuery({ + const { data: encounter, isLoading: encounterLoading } = useQuery({ queryKey: ["encounter", encounterId], queryFn: query(api.encounter.get, { pathParams: { id: encounterId }, queryParams: { facility: facilityId }, }), + enabled: !!encounterId && !!facilityId, }); + const { data: allergies, isLoading: allergiesLoading } = useQuery({ + queryKey: ["allergies", patientId, encounterId], + queryFn: query.paginated(allergyIntoleranceApi.getAllergy, { + pathParams: { patientId }, + queryParams: { + encounter: ( + encounter?.status + ? completedEncounterStatus.includes(encounter.status) + : false + ) + ? encounterId + : undefined, + }, + pageSize: 100, + }), + }); + + const { data: symptoms, isLoading: symptomsLoading } = useQuery({ + queryKey: ["symptoms", patientId, encounterId], + queryFn: query.paginated(symptomApi.listSymptoms, { + pathParams: { patientId }, + queryParams: { encounter: encounterId }, + pageSize: 100, + }), + enabled: !!patientId && !!encounterId, + }); + + const { data: diagnoses, isLoading: diagnosesLoading } = useQuery({ + queryKey: ["diagnosis", patientId, encounterId], + queryFn: query.paginated(diagnosisApi.listDiagnosis, { + pathParams: { patientId }, + queryParams: { encounter: encounterId }, + pageSize: 100, + }), + enabled: !!patientId && !!encounterId, + }); + + const { data: medications, isLoading: medicationsLoading } = useQuery({ + queryKey: ["medication_requests", patientId, encounterId], + queryFn: query.paginated(medicationRequestApi.list, { + pathParams: { patientId }, + queryParams: { encounter: encounterId }, + pageSize: 100, + }), + enabled: !!encounterId, + }); + const { data: medicationStatement, isLoading: medicationStatementLoading } = + useQuery({ + queryKey: ["medication_statements", patientId], + queryFn: query.paginated(medicationStatementApi.list, { + pathParams: { patientId }, + pageSize: 100, + }), + enabled: !!patientId, + }); + + if (encounterLoading) { + return ; + } + if (!encounter) { return (
    @@ -42,11 +137,31 @@ export default function TreatmentSummary({ ); } + const isLoading = + encounterLoading || + allergiesLoading || + diagnosesLoading || + symptomsLoading || + medicationsLoading || + medicationStatementLoading; + + if (isLoading) { + return ( + +
    + +
    +
    + ); + } + return ( -
    +
    {/* Header */}
    @@ -68,36 +183,39 @@ export default function TreatmentSummary({
    {/* Patient Details */} -
    +
    -
    +
    {t("patient")} : - {encounter.patient.name} + + {encounter.patient.name} +
    -
    +
    {`${t("age")} / ${t("sex")}`} : - + {`${formatPatientAge(encounter.patient, true)}, ${t(`GENDER__${encounter.patient.gender}`)}`}
    -
    +
    {t("encounter_class")} : {t(`encounter_class__${encounter.encounter_class}`)}
    -
    +
    {t("priority")} : {t(`encounter_priority__${encounter.priority}`)}
    + {encounter.hospitalization?.admit_source && ( -
    +
    {t("admission_source")} : @@ -108,14 +226,14 @@ export default function TreatmentSummary({
    )} {encounter.hospitalization?.re_admission && ( -
    +
    {t("readmission")} : {t("yes")}
    )} {encounter.hospitalization?.diet_preference && ( -
    +
    {t("diet_preference")} : @@ -126,16 +244,19 @@ export default function TreatmentSummary({
    )}
    + + {/* Right Column */}
    -
    +
    {t("mobile_number")} : - + {encounter.patient.phone_number}
    + {encounter.period?.start && ( -
    +
    {t("encounter_date")} : @@ -146,22 +267,25 @@ export default function TreatmentSummary({
    )} -
    + +
    {t("status")} : {t(`encounter_status__${encounter.status}`)}
    -
    + +
    {t("consulting_doctor")} : {formatName(encounter.created_by)}
    + {encounter.external_identifier && ( -
    +
    {t("external_id")} : @@ -169,8 +293,9 @@ export default function TreatmentSummary({
    )} + {encounter.hospitalization?.discharge_disposition && ( -
    +
    {t("discharge_disposition")} @@ -184,51 +309,176 @@ export default function TreatmentSummary({ )}
    - {/* Medical Information */}
    {/* Allergies */} - + + {allergies?.count ? ( + ({ + allergen: allergy.code.display, + status: t(allergy.clinical_status), + criticality: t(allergy.criticality), + verification: t(allergy.verification_status), + notes: allergy.note, + logged_by: formatName(allergy.created_by), + }))} + /> + ) : ( + + )} + {/* Symptoms */} - + + + {symptoms?.count ? ( + ({ + symptom: symptom.code.display, + severity: t(symptom.severity), + status: t(symptom.clinical_status), + verification: t(symptom.verification_status), + onset: symptom.onset?.onset_datetime + ? new Date( + symptom.onset.onset_datetime, + ).toLocaleDateString() + : "-", + notes: symptom.note, + logged_by: formatName(symptom.created_by), + }))} + /> + ) : ( + + )} + {/* Diagnoses */} - + + {diagnoses?.count ? ( + ({ + diagnosis: diagnosis.code.display, + status: t(diagnosis.clinical_status), + verification: t(diagnosis.verification_status), + onset: diagnosis.onset?.onset_datetime + ? new Date( + diagnosis.onset.onset_datetime, + ).toLocaleDateString() + : undefined, + notes: diagnosis.note, + logged_by: formatName(diagnosis.created_by), + }))} + /> + ) : ( + + )} + {/* Medications */} -
    -

    - {t("medications")} -

    - -
    -
    + + {medications?.results.length ? ( + { + const instruction = medication.dosage_instruction[0]; + const frequency = getFrequencyDisplay(instruction?.timing); + const dosage = formatDosage(instruction); + const duration = + instruction?.timing?.repeat?.bounds_duration; + const remarks = formatSig(instruction); + const notes = medication.note; + return { + medicine: medication.medication?.display, + status: t(medication.status), + dosage: dosage, + frequency: instruction?.as_needed_boolean + ? `${t("as_needed_prn")} (${instruction?.as_needed_for?.display ?? "-"})` + : (frequency?.meaning ?? "-") + + (instruction?.additional_instruction?.[0]?.display + ? `, ${instruction.additional_instruction[0].display}` + : ""), + duration: duration + ? `${duration.value} ${duration.unit}` + : "-", + instructions: `${remarks || "-"}${notes ? ` (${t("note")}: ${notes})` : ""}`, + }; + })} + /> + ) : ( + + )} + - {/* Medication Statements */} - + {/* Medication Statements */} + + {medicationStatement?.results.length ? ( + ({ + medication: + medication.medication.display ?? + medication.medication.code, + dosage: medication.dosage_text, + status: medication.status, + medication_taken_between: [ + medication.effective_period?.start, + medication.effective_period?.end, + ] + .map((date) => formatDateTime(date)) + .join(" - "), + reason: medication.reason, + notes: medication.note, + logged_by: formatName(medication.created_by), + }))} + /> + ) : ( + + )} + +
    {/* Questionnaire Responses Section */}
    diff --git a/src/components/Patient/allergy/list.tsx b/src/components/Patient/allergy/list.tsx index 62695fe02e8..4e4fe877a90 100644 --- a/src/components/Patient/allergy/list.tsx +++ b/src/components/Patient/allergy/list.tsx @@ -50,7 +50,7 @@ interface AllergyListProps { patientId: string; encounterId?: string; className?: string; - isPrintPreview?: boolean; + encounterStatus?: Encounter["status"]; } @@ -72,10 +72,9 @@ export function AllergyList({ patientId, encounterId, className, - isPrintPreview = false, encounterStatus, }: AllergyListProps) { - const [showEnteredInError, setShowEnteredInError] = useState(isPrintPreview); + const [showEnteredInError, setShowEnteredInError] = useState(false); const { data: allergies, isLoading } = useQuery({ queryKey: ["allergies", patientId, encounterId, encounterStatus], @@ -178,28 +177,22 @@ export function AllergyList({ {allergy.note && (
    - {isPrintPreview ? ( - - {allergy.note} - - ) : ( - - - - - -

    - {allergy.note} -

    -
    -
    - )} + + + + + +

    + {allergy.note} +

    +
    +
    )}
    @@ -223,7 +216,6 @@ export function AllergyList({ patientId={patientId} encounterId={encounterId} className={className} - isPrintPreview={isPrintPreview} >
    @@ -295,24 +287,16 @@ const AllergyListLayout = ({ encounterId, children, className, - isPrintPreview = false, }: { facilityId?: string; patientId: string; encounterId?: string; children: ReactNode; className?: string; - isPrintPreview?: boolean; }) => { return ( - + {t("allergies")} {facilityId && encounterId && ( )} - - {children} - + {children} ); }; diff --git a/src/components/Patient/diagnosis/DiagnosisTable.tsx b/src/components/Patient/diagnosis/DiagnosisTable.tsx index ac9d93cc878..b80e0b6062a 100644 --- a/src/components/Patient/diagnosis/DiagnosisTable.tsx +++ b/src/components/Patient/diagnosis/DiagnosisTable.tsx @@ -26,13 +26,9 @@ import { interface DiagnosisTableProps { diagnoses: Diagnosis[]; - isPrintPreview?: boolean; } -export function DiagnosisTable({ - diagnoses, - isPrintPreview = false, -}: DiagnosisTableProps) { +export function DiagnosisTable({ diagnoses }: DiagnosisTableProps) { return (
    @@ -100,26 +96,22 @@ export function DiagnosisTable({ {diagnosis.note ? (
    - {isPrintPreview ? ( - {diagnosis.note} - ) : ( - - - - - -

    - {diagnosis.note} -

    -
    -
    - )} + + + + + +

    + {diagnosis.note} +

    +
    +
    ) : ( "-" @@ -132,6 +124,7 @@ export function DiagnosisTable({ className="w-4 h-4" imageUrl={diagnosis.created_by.profile_picture_url} /> + {diagnosis.created_by.username}
    diff --git a/src/components/Patient/diagnosis/list.tsx b/src/components/Patient/diagnosis/list.tsx index 4695f5900f9..cb682456b02 100644 --- a/src/components/Patient/diagnosis/list.tsx +++ b/src/components/Patient/diagnosis/list.tsx @@ -21,7 +21,6 @@ interface DiagnosisListProps { encounterId?: string; facilityId?: string; className?: string; - isPrintPreview?: boolean; } export function DiagnosisList({ @@ -29,9 +28,8 @@ export function DiagnosisList({ encounterId, facilityId, className, - isPrintPreview = false, }: DiagnosisListProps) { - const [showEnteredInError, setShowEnteredInError] = useState(isPrintPreview); + const [showEnteredInError, setShowEnteredInError] = useState(false); const { data: diagnoses, isLoading } = useQuery({ queryKey: ["diagnosis", patientId, encounterId], @@ -47,6 +45,7 @@ export function DiagnosisList({ facilityId={facilityId} patientId={patientId} encounterId={encounterId} + className={className} > @@ -71,6 +70,7 @@ export function DiagnosisList({ facilityId={facilityId} patientId={patientId} encounterId={encounterId} + className={className} >

    {t("no_diagnoses_recorded")}

    @@ -85,38 +85,39 @@ export function DiagnosisList({ patientId={patientId} encounterId={encounterId} className={className} - isPrintPreview={isPrintPreview} > - diagnosis.verification_status !== "entered_in_error", - ), - ...(showEnteredInError - ? filteredDiagnoses.filter( - (diagnosis) => - diagnosis.verification_status === "entered_in_error", - ) - : []), - ]} - isPrintPreview={isPrintPreview} - /> + <> + + diagnosis.verification_status !== "entered_in_error", + ), + ...(showEnteredInError + ? filteredDiagnoses.filter( + (diagnosis) => + diagnosis.verification_status === "entered_in_error", + ) + : []), + ]} + /> - {hasEnteredInErrorRecords && !showEnteredInError && ( - <> -
    -
    - -
    - - )} + {hasEnteredInErrorRecords && !showEnteredInError && ( + <> +
    +
    + +
    + + )} + ); } @@ -127,22 +128,17 @@ const DiagnosisListLayout = ({ encounterId, children, className, - isPrintPreview = false, }: { facilityId?: string; patientId: string; encounterId?: string; children: ReactNode; className?: string; - isPrintPreview?: boolean; }) => { return ( - + {t("diagnoses")} {facilityId && encounterId && ( @@ -155,14 +151,7 @@ const DiagnosisListLayout = ({ )} - - {children} - + {children} ); }; diff --git a/src/components/Patient/symptoms/SymptomTable.tsx b/src/components/Patient/symptoms/SymptomTable.tsx index 407a3300749..e3b3ec76cbc 100644 --- a/src/components/Patient/symptoms/SymptomTable.tsx +++ b/src/components/Patient/symptoms/SymptomTable.tsx @@ -27,13 +27,9 @@ import { interface SymptomTableProps { symptoms: Symptom[]; - isPrintPreview?: boolean; } -export function SymptomTable({ - symptoms, - isPrintPreview = false, -}: SymptomTableProps) { +export function SymptomTable({ symptoms }: SymptomTableProps) { return (
    @@ -118,26 +114,22 @@ export function SymptomTable({ {symptom.note ? (
    - {isPrintPreview ? ( - {symptom.note} - ) : ( - - - - - -

    - {symptom.note} -

    -
    -
    - )} + + + + + +

    + {symptom.note} +

    +
    +
    ) : ( "-" @@ -150,6 +142,7 @@ export function SymptomTable({ className="w-4 h-4" imageUrl={symptom.created_by.profile_picture_url} /> + {symptom.created_by.username}
    diff --git a/src/components/Patient/symptoms/list.tsx b/src/components/Patient/symptoms/list.tsx index 0f4b558d6a9..891154584f1 100644 --- a/src/components/Patient/symptoms/list.tsx +++ b/src/components/Patient/symptoms/list.tsx @@ -21,7 +21,6 @@ interface SymptomsListProps { encounterId?: string; facilityId?: string; className?: string; - isPrintPreview?: boolean; } export function SymptomsList({ @@ -29,9 +28,8 @@ export function SymptomsList({ encounterId, facilityId, className, - isPrintPreview = false, }: SymptomsListProps) { - const [showEnteredInError, setShowEnteredInError] = useState(isPrintPreview); + const [showEnteredInError, setShowEnteredInError] = useState(false); const { data: symptoms, isLoading } = useQuery({ queryKey: ["symptoms", patientId, encounterId], @@ -84,7 +82,6 @@ export function SymptomsList({ patientId={patientId} encounterId={encounterId} className={className} - isPrintPreview={isPrintPreview} > {hasEnteredInErrorRecords && !showEnteredInError && ( @@ -125,24 +121,16 @@ const SymptomListLayout = ({ encounterId, children, className, - isPrintPreview = false, }: { facilityId?: string; patientId: string; encounterId?: string; children: ReactNode; className?: string; - isPrintPreview?: boolean; }) => { return ( - + {t("symptoms")} {facilityId && encounterId && ( )} - - {children} - + {children} ); }; From 6c95ea471e05551c6996259eac08ec863e2ca8f7 Mon Sep 17 00:00:00 2001 From: Rithvik Nishad Date: Thu, 27 Feb 2025 07:01:41 +0000 Subject: [PATCH 05/21] API queries: fail fast and retry only for network errors and HTTP 502/503/504s (#10837) --- src/App.tsx | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/App.tsx b/src/App.tsx index 51e33c2209d..7452d7afb23 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -18,13 +18,23 @@ import AuthUserProvider from "@/Providers/AuthUserProvider"; import HistoryAPIProvider from "@/Providers/HistoryAPIProvider"; import Routers from "@/Routers"; import { handleHttpError } from "@/Utils/request/errorHandler"; +import { HTTPError } from "@/Utils/request/types"; import { PubSubProvider } from "./Utils/pubsubContext"; const queryClient = new QueryClient({ defaultOptions: { queries: { - retry: 2, + retry: (failureCount, error) => { + // Only retry network errors or server errors (502, 503, 504) up to 3 times + if ( + error.message === "Network Error" || + (error instanceof HTTPError && [502, 503, 504].includes(error.status)) + ) { + return failureCount < 3; + } + return false; + }, refetchOnWindowFocus: false, }, }, From 7bb209d1cb749d9675caee0be4ca5b3a00fcc39f Mon Sep 17 00:00:00 2001 From: Jacob John Jeevan <40040905+Jacobjeevan@users.noreply.github.com> Date: Thu, 27 Feb 2025 14:04:59 +0530 Subject: [PATCH 06/21] Location Edit Form and Files Fixes (#10751) --- src/components/Files/FilesTab.tsx | 102 +++++++++++++++--------------- 1 file changed, 50 insertions(+), 52 deletions(-) diff --git a/src/components/Files/FilesTab.tsx b/src/components/Files/FilesTab.tsx index 50fb179cd36..3c1abcbfb45 100644 --- a/src/components/Files/FilesTab.tsx +++ b/src/components/Files/FilesTab.tsx @@ -251,54 +251,52 @@ export const FilesTab = (props: FilesTabProps) => { const filetype = getFileType(file); return ( <> - {editPermission() && ( -
    - {filetype === "AUDIO" && !file.is_archived && ( - - )} - {fileManager.isPreviewable(file) && ( - + )} + {fileManager.isPreviewable(file) && ( + + )} + + + - )} - { - - - - - - - - + + + + + + {editPermission() && ( + <> - - - } -
    - )} + + )} + + + ); }; From 4748ab70c392d2e6a54f76fea4e51cc7c364c61b Mon Sep 17 00:00:00 2001 From: Kamishetty Rishith <119791436+Rishith25@users.noreply.github.com> Date: Thu, 27 Feb 2025 15:58:10 +0530 Subject: [PATCH 07/21] Add tooltips to facility, organization, and patient switchers (#10766) --- .../ui/sidebar/facility-switcher.tsx | 1 + .../ui/sidebar/organization-switcher.tsx | 5 + .../ui/sidebar/patient-switcher.tsx | 92 +++++++++++-------- 3 files changed, 61 insertions(+), 37 deletions(-) diff --git a/src/components/ui/sidebar/facility-switcher.tsx b/src/components/ui/sidebar/facility-switcher.tsx index 8cbb974d1ec..51bc2c72b9a 100644 --- a/src/components/ui/sidebar/facility-switcher.tsx +++ b/src/components/ui/sidebar/facility-switcher.tsx @@ -40,6 +40,7 @@ export function FacilitySwitcher({
    diff --git a/src/components/ui/sidebar/organization-switcher.tsx b/src/components/ui/sidebar/organization-switcher.tsx index ff766f12b21..7bbf1ff8e53 100644 --- a/src/components/ui/sidebar/organization-switcher.tsx +++ b/src/components/ui/sidebar/organization-switcher.tsx @@ -42,6 +42,11 @@ export function OrganizationSwitcher({
    diff --git a/src/components/ui/sidebar/patient-switcher.tsx b/src/components/ui/sidebar/patient-switcher.tsx index 97810946b03..ce7f393b2da 100644 --- a/src/components/ui/sidebar/patient-switcher.tsx +++ b/src/components/ui/sidebar/patient-switcher.tsx @@ -10,6 +10,11 @@ import { SelectValue, } from "@/components/ui/select"; import { useSidebar } from "@/components/ui/sidebar"; +import { + Tooltip, + TooltipContent, + TooltipTrigger, +} from "@/components/ui/tooltip"; import { Avatar } from "@/components/Common/Avatar"; @@ -48,43 +53,56 @@ export function PatientSwitcher({ className }: PatientSwitcherProps) { } }} > - - - <> - {open && ( -
    - -
    - - {patientUserContext.selectedPatient?.name} - - - {t("switch")} - -
    -
    - )} - {!open && ( -
    - -
    - )} - -
    -
    + + + + + <> + {open && ( +
    + +
    + + {patientUserContext.selectedPatient?.name} + + + {t("switch")} + +
    +
    + )} + {!open && ( +
    + +
    + )} + +
    +
    +
    + {!open && ( + +

    {patientUserContext.selectedPatient?.name}

    +
    + )} +
    {patientUserContext.patients?.map((patient) => ( From 5c0a73c31c3cd958292147f7c4694f1c84d0bb63 Mon Sep 17 00:00:00 2001 From: Kamishetty Rishith <119791436+Rishith25@users.noreply.github.com> Date: Thu, 27 Feb 2025 15:59:25 +0530 Subject: [PATCH 08/21] Fix: Disable submit/save buttons until changes are made on any Edit/Update details page (#10764) --- src/components/Facility/FacilityForm.tsx | 20 ++++++++++++---- .../Patient/PatientRegistration.tsx | 22 +++++++++++++----- src/components/Resource/ResourceForm.tsx | 23 +++++++++++++++---- .../settings/locations/LocationForm.tsx | 7 +++++- .../components/EditScheduleTemplateSheet.tsx | 2 +- 5 files changed, 57 insertions(+), 17 deletions(-) diff --git a/src/components/Facility/FacilityForm.tsx b/src/components/Facility/FacilityForm.tsx index c6cabe1de6a..1058f324bd0 100644 --- a/src/components/Facility/FacilityForm.tsx +++ b/src/components/Facility/FacilityForm.tsx @@ -144,7 +144,7 @@ export default function FacilityForm({ const handleFeatureChange = (value: string[]) => { const features = value.map((val) => Number(val)); - form.setValue("features", features); + form.setValue("features", features, { shouldDirty: true }); }; const handleGetCurrentLocation = () => { @@ -152,8 +152,12 @@ export default function FacilityForm({ setIsGettingLocation(true); navigator.geolocation.getCurrentPosition( (position) => { - form.setValue("latitude", position.coords.latitude); - form.setValue("longitude", position.coords.longitude); + form.setValue("latitude", position.coords.latitude, { + shouldDirty: true, + }); + form.setValue("longitude", position.coords.longitude, { + shouldDirty: true, + }); setIsGettingLocation(false); toast.success(t("location_updated_successfully")); }, @@ -346,7 +350,9 @@ export default function FacilityForm({ value={form.watch("geo_organization")} selected={selectedLevels} onChange={(value) => - form.setValue("geo_organization", value) + form.setValue("geo_organization", value, { + shouldDirty: true, + }) } required /> @@ -418,6 +424,7 @@ export default function FacilityForm({ form.setValue( "latitude", e.target.value ? Number(e.target.value) : undefined, + { shouldDirty: true }, ); }} data-cy="facility-latitude" @@ -445,6 +452,7 @@ export default function FacilityForm({ form.setValue( "longitude", e.target.value ? Number(e.target.value) : undefined, + { shouldDirty: true }, ); }} data-cy="facility-longitude" @@ -493,7 +501,9 @@ export default function FacilityForm({ type="submit" className="w-full" variant="primary" - disabled={facilityId ? isUpdatePending : isPending} + disabled={ + facilityId ? isUpdatePending || !form.formState.isDirty : isPending + } data-cy={facilityId ? "update-facility" : "submit-facility"} > {facilityId ? ( diff --git a/src/components/Patient/PatientRegistration.tsx b/src/components/Patient/PatientRegistration.tsx index 6f46b141468..bfc08e2f435 100644 --- a/src/components/Patient/PatientRegistration.tsx +++ b/src/components/Patient/PatientRegistration.tsx @@ -262,26 +262,32 @@ export default function PatientRegistration( patientQuery.data.geo_organization as unknown as Organization, ]); form.reset({ - ...patientQuery.data, + name: patientQuery.data.name || "", + phone_number: patientQuery.data.phone_number || "", + emergency_phone_number: patientQuery.data.emergency_phone_number || "", same_phone_number: patientQuery.data.phone_number === patientQuery.data.emergency_phone_number, same_address: patientQuery.data.address === patientQuery.data.permanent_address, + gender: patientQuery.data.gender as (typeof GENDERS)[number], + blood_group: patientQuery.data.blood_group, age_or_dob: patientQuery.data.date_of_birth ? "dob" : "age", + date_of_birth: patientQuery.data.date_of_birth || undefined, age: !patientQuery.data.date_of_birth && patientQuery.data.year_of_birth ? new Date().getFullYear() - patientQuery.data.year_of_birth : undefined, - date_of_birth: patientQuery.data.date_of_birth - ? patientQuery.data.date_of_birth - : undefined, + address: patientQuery.data.address || "", + permanent_address: patientQuery.data.permanent_address || "", + pincode: patientQuery.data.pincode || undefined, + nationality: patientQuery.data.nationality || "India", geo_organization: ( patientQuery.data.geo_organization as unknown as Organization )?.id, } as unknown as z.infer); } - }, [patientQuery.data]); // eslint-disable-line react-hooks/exhaustive-deps + }, [patientQuery.data]); const showDuplicate = !patientPhoneSearch.isLoading && @@ -733,7 +739,11 @@ export default function PatientRegistration( diff --git a/src/components/Resource/ResourceForm.tsx b/src/components/Resource/ResourceForm.tsx index bc4981b2ab9..fa443f489e0 100644 --- a/src/components/Resource/ResourceForm.tsx +++ b/src/components/Resource/ResourceForm.tsx @@ -202,7 +202,7 @@ export default function ResourceForm({ facilityId, id }: ResourceProps) { })); const handleUserChange = (user: UserBase) => { - form.setValue("assigned_to", user.id); + form.setValue("assigned_to", user.id, { shouldDirty: true }); setAssignedToUser(user); }; @@ -210,9 +210,14 @@ export default function ResourceForm({ facilityId, id }: ResourceProps) { form.setValue( "referring_facility_contact_name", `${authUser.first_name} ${authUser.last_name}`.trim(), + { shouldDirty: true }, ); if (authUser.phone_number) { - form.setValue("referring_facility_contact_number", authUser.phone_number); + form.setValue( + "referring_facility_contact_number", + authUser.phone_number, + { shouldDirty: true }, + ); } }; @@ -292,7 +297,9 @@ export default function ResourceForm({ facilityId, id }: ResourceProps) { facilities?.results.find( (f) => f.id === value, ) ?? null; - form.setValue("assigned_facility", facility); + form.setValue("assigned_facility", facility, { + shouldDirty: true, + }); }} /> @@ -551,7 +558,15 @@ export default function ResourceForm({ facilityId, id }: ResourceProps) { > {t("cancel")} -
    -
    diff --git a/src/components/Facility/FacilityMapLink.tsx b/src/components/Facility/FacilityMapLink.tsx new file mode 100644 index 00000000000..47a76082fbf --- /dev/null +++ b/src/components/Facility/FacilityMapLink.tsx @@ -0,0 +1,42 @@ +import { SquareArrowOutUpRight } from "lucide-react"; +import { Link } from "raviger"; +import { useTranslation } from "react-i18next"; + +import { getMapUrl, isAndroidDevice } from "@/Utils/utils"; + +const isValidLatitude = (latitude: string) => { + const lat = parseFloat(latitude.trim()); + return Number.isFinite(lat) && lat >= -90 && lat <= 90; +}; + +const isValidLongitude = (longitude: string) => { + const long = parseFloat(longitude.trim()); + return Number.isFinite(long) && long >= -180 && long <= 180; +}; + +export const FacilityMapsLink = ({ + latitude, + longitude, +}: { + latitude: string; + longitude: string; +}) => { + const { t } = useTranslation(); + + if (!isValidLatitude(latitude) || !isValidLongitude(longitude)) { + return null; + } + const target = isAndroidDevice ? "_self" : "_blank"; + + return ( + + {t("show_on_map")} + + + ); +}; diff --git a/src/pages/Facility/FacilityDetailsPage.tsx b/src/pages/Facility/FacilityDetailsPage.tsx index 228d203f61f..b1c5f8497f4 100644 --- a/src/pages/Facility/FacilityDetailsPage.tsx +++ b/src/pages/Facility/FacilityDetailsPage.tsx @@ -10,6 +10,7 @@ import { Markdown } from "@/components/ui/markdown"; import { Avatar } from "@/components/Common/Avatar"; import { LoginHeader } from "@/components/Common/LoginHeader"; +import { FacilityMapsLink } from "@/components/Facility/FacilityMapLink"; import { FacilityModel } from "@/components/Facility/models"; import { UserAssignedModel } from "@/components/Users/models"; @@ -115,6 +116,12 @@ export function FacilityDetailsPage({ id }: Props) {

    {facility.name}

    {[facility.address].filter(Boolean).join(", ")} + {facility.latitude && facility.longitude && ( + + )}

    diff --git a/src/pages/Facility/components/FacilityCard.tsx b/src/pages/Facility/components/FacilityCard.tsx index 2ba7c030ad7..227b9ca892c 100644 --- a/src/pages/Facility/components/FacilityCard.tsx +++ b/src/pages/Facility/components/FacilityCard.tsx @@ -6,6 +6,7 @@ import { Button } from "@/components/ui/button"; import { Card } from "@/components/ui/card"; import { Avatar } from "@/components/Common/Avatar"; +import { FacilityMapsLink } from "@/components/Facility/FacilityMapLink"; import { FeatureBadge } from "@/pages/Facility/Utils"; import { FacilityData } from "@/types/facility/facility"; @@ -36,6 +37,12 @@ export function FacilityCard({ facility, className }: Props) { {facility.facility_type?.name}

    {[facility.address].filter(Boolean).join(", ")} + {facility.latitude && facility.longitude && ( + + )}

    diff --git a/src/vite-env.d.ts b/src/vite-env.d.ts index 5e2066155a8..e6fdceb00b6 100644 --- a/src/vite-env.d.ts +++ b/src/vite-env.d.ts @@ -33,6 +33,7 @@ interface ImportMetaEnv { readonly REACT_ENABLE_ABDM?: string; readonly REACT_ENABLE_SCRIBE?: string; readonly REACT_DEFAULT_COUNTRY?: string; + readonly REACT_MAPS_FALLBACK_URL_TEMPLATE?: string; } interface ImportMeta { From cdb10f936e162d1a7b707fe6161ae563740f8909 Mon Sep 17 00:00:00 2001 From: Vinu Date: Thu, 27 Feb 2025 18:14:05 +0530 Subject: [PATCH 10/21] Fix unnecessary scrollbar on facility overview page (#10833) --- src/Routers/AppRouter.tsx | 13 +++++-------- src/components/Common/Page.tsx | 2 +- src/components/ui/sidebar.tsx | 12 ++++++------ src/pages/Facility/overview.tsx | 18 +++++++++--------- 4 files changed, 21 insertions(+), 24 deletions(-) diff --git a/src/Routers/AppRouter.tsx b/src/Routers/AppRouter.tsx index 5461309083a..353e58054f4 100644 --- a/src/Routers/AppRouter.tsx +++ b/src/Routers/AppRouter.tsx @@ -106,24 +106,21 @@ export default function AppRouter() { )}
    -
    +
    {shouldShowSidebar && }
    - + care logo
    -
    +
    }> {pages} diff --git a/src/components/Common/Page.tsx b/src/components/Common/Page.tsx index 0bb287d8637..a4a2ba48a3e 100644 --- a/src/components/Common/Page.tsx +++ b/src/components/Common/Page.tsx @@ -31,7 +31,7 @@ export default function Page(props: PageProps) { // }, [props.collapseSidebar]); return ( -
    +
    Sidebar -
    {children}
    +
    {children}
    ); @@ -220,7 +220,7 @@ const Sidebar = React.forwardRef< return (
    {isMobile ? ( {showIcon && ( )} diff --git a/src/pages/Facility/overview.tsx b/src/pages/Facility/overview.tsx index cf07db69e65..28c623dca3e 100644 --- a/src/pages/Facility/overview.tsx +++ b/src/pages/Facility/overview.tsx @@ -35,10 +35,10 @@ export function FacilityOverview({ facilityId }: FacilityOverviewProps) { ]; return ( -
    -
    +
    +
    {/* Welcome Header */} -
    +

    @@ -53,20 +53,20 @@ export function FacilityOverview({ facilityId }: FacilityOverviewProps) { {/* Quick Actions Section */}
    -

    +

    {t("quick_actions")}

    -
    +
    {shortcuts.map((shortcut) => ( - - -
    - + + +
    +
    From 9b82db36bb7748db8759153819cf7348fb0e5f2d Mon Sep 17 00:00:00 2001 From: Amjith Titus Date: Thu, 27 Feb 2025 18:14:46 +0530 Subject: [PATCH 11/21] Encounter Info Card (#10792) --- .../Patient/EncounterQuestionnaire.tsx | 80 +++++-- src/components/Patient/PatientInfoCard.tsx | 224 ++++++++++-------- 2 files changed, 181 insertions(+), 123 deletions(-) diff --git a/src/components/Patient/EncounterQuestionnaire.tsx b/src/components/Patient/EncounterQuestionnaire.tsx index 90de3924f2c..10799afe296 100644 --- a/src/components/Patient/EncounterQuestionnaire.tsx +++ b/src/components/Patient/EncounterQuestionnaire.tsx @@ -1,13 +1,19 @@ +import { useQuery } from "@tanstack/react-query"; import { t } from "i18next"; import { navigate } from "raviger"; import { Card, CardContent } from "@/components/ui/card"; import Page from "@/components/Common/Page"; +import PatientInfoCard from "@/components/Patient/PatientInfoCard"; import { QuestionnaireForm } from "@/components/Questionnaire/QuestionnaireForm"; import useAppHistory from "@/hooks/useAppHistory"; +import routes from "@/Utils/request/api"; +import query from "@/Utils/request/query"; +import { formatDateTime } from "@/Utils/utils"; + interface Props { facilityId: string; patientId: string; @@ -24,29 +30,61 @@ export default function EncounterQuestionnaire({ subjectType, }: Props) { const { goBack } = useAppHistory(); + const { data: encounter } = useQuery({ + queryKey: ["encounter", encounterId], + queryFn: query(routes.encounter.get, { + pathParams: { id: encounterId ?? "" }, + queryParams: { facility: facilityId }, + }), + enabled: !!encounterId, + }); return ( - - - { - if (encounterId) { - navigate( - `/facility/${facilityId}/patient/${patientId}/encounter/${encounterId}/updates`, - ); - } else { - navigate(`/patient/${patientId}/updates`); - } - }} - onCancel={() => goBack()} - /> - - +
    + {encounter && ( +
    + {}} + disableButtons={true} + /> + +
    +
    +
    + + {t("last_modified")}:{" "} + +   + {formatDateTime(encounter.modified_date)} +
    +
    +
    +
    + )} + + + { + if (encounterId) { + navigate( + `/facility/${facilityId}/patient/${patientId}/encounter/${encounterId}/updates`, + ); + } else { + navigate(`/patient/${patientId}/updates`); + } + }} + onCancel={() => goBack()} + /> + + +
    ); } diff --git a/src/components/Patient/PatientInfoCard.tsx b/src/components/Patient/PatientInfoCard.tsx index 1212a95300d..1e9b2922126 100644 --- a/src/components/Patient/PatientInfoCard.tsx +++ b/src/components/Patient/PatientInfoCard.tsx @@ -45,6 +45,7 @@ import { import { Avatar } from "@/components/Common/Avatar"; import { LocationHistorySheet } from "@/components/Location/LocationHistorySheet"; import { LocationTree } from "@/components/Location/LocationTree"; +import LinkDepartmentsSheet from "@/components/Patient/LinkDepartmentsSheet"; import { PLUGIN_Component } from "@/PluginEngine"; import routes from "@/Utils/request/api"; @@ -52,17 +53,17 @@ import mutate from "@/Utils/request/mutate"; import { formatDateTime, formatPatientAge } from "@/Utils/utils"; import { Encounter, completedEncounterStatus } from "@/types/emr/encounter"; import { Patient } from "@/types/emr/newPatient"; - -import LinkDepartmentsSheet from "./LinkDepartmentsSheet"; +import { FacilityOrganization } from "@/types/facilityOrganization/facilityOrganization"; export interface PatientInfoCardProps { patient: Patient; encounter: Encounter; fetchPatientData?: (state: { aborted: boolean }) => void; + disableButtons?: boolean; } export default function PatientInfoCard(props: PatientInfoCardProps) { - const { patient, encounter } = props; + const { patient, encounter, disableButtons = false } = props; const { t } = useTranslation(); const queryClient = useQueryClient(); @@ -331,17 +332,9 @@ export default function PatientInfoCard(props: PatientInfoCardProps) { facilityId={encounter.facility.id} trigger={
    - {encounter.organizations.map((org) => ( - - - {org.name} - - ))} + {encounter.organizations.map((org) => + organizationBadge(org), + )} {encounter.organizations.length === 0 && ( -
    - + {!disableButtons && ( + <> +
    + + + )}
    ) : ( - - - - {t("add_location")} - - + encounter.status !== "completed" && + !disableButtons && ( + + + + {t("add_location")} + + + ) )}
    @@ -439,78 +439,98 @@ export default function PatientInfoCard(props: PatientInfoCardProps) { className="flex flex-col items-center justify-end gap-4 px-4 py-1 2xl:flex-row" id="consultation-buttons" > - {!completedEncounterStatus.includes(encounter.status) && ( -
    - - - - - - - {t("actions")} - - - {t("treatment_summary")} - - - - - {t("discharge_summary")} - - - - e.preventDefault()}> - {t("mark_as_complete")} + {!completedEncounterStatus.includes(encounter.status) && + !disableButtons && ( +
    + + + + + + + {t("actions")} + + + {t("treatment_summary")} + - + + + {t("discharge_summary")} + + + + e.preventDefault()}> + {t("mark_as_complete")} + + + + + + + + + {t("mark_as_complete")} + + + {t("mark_encounter_as_complete_confirmation")} + + + - - - - - {t("mark_as_complete")} - - {t("mark_encounter_as_complete_confirmation")} - - - - - - {t("cancel")} + + {t("cancel")} - - {t("mark_as_complete")} - - - - -
    - )} + + {t("mark_as_complete")} + + + +
    +
    + )}
    ); + + function organizationBadge(org: FacilityOrganization) { + return ( + + + {org.name} + + ); + } } From 5ffc6b3abd12d7df93dd2e36cc5a52a492568a3c Mon Sep 17 00:00:00 2001 From: Don Xavier <98073418+DonXavierdev@users.noreply.github.com> Date: Thu, 27 Feb 2025 18:45:12 +0530 Subject: [PATCH 12/21] Cypress: User Avatar Modification (#10592) --- cypress/e2e/users_spec/user_avatar.cy.ts | 22 ++++++++ cypress/fixtures/avatar.jpg | Bin 0 -> 48263 bytes cypress/fixtures/users.json | 4 ++ cypress/pageObject/Users/UserAvatar.ts | 59 ++++++++++++++++++++++ src/components/Common/AvatarEditModal.tsx | 2 + src/components/Users/UserAvatar.tsx | 2 + 6 files changed, 89 insertions(+) create mode 100644 cypress/e2e/users_spec/user_avatar.cy.ts create mode 100644 cypress/fixtures/avatar.jpg create mode 100644 cypress/pageObject/Users/UserAvatar.ts diff --git a/cypress/e2e/users_spec/user_avatar.cy.ts b/cypress/e2e/users_spec/user_avatar.cy.ts new file mode 100644 index 00000000000..830dcb1b40c --- /dev/null +++ b/cypress/e2e/users_spec/user_avatar.cy.ts @@ -0,0 +1,22 @@ +import { UserAvatar } from "@/pageObject/Users/UserAvatar"; + +describe("User Profile Avatar Modification", () => { + const userAvatar = new UserAvatar("teststaff4"); + beforeEach(() => { + cy.loginByApi("teststaff4"); + cy.visit("/"); + }); + it("should modify an avatar", () => { + userAvatar + .navigateToProfile() + .interceptUploadAvatarRequest() + .clickChangeAvatarButton() + .uploadAvatar() + .clickSaveAvatarButton() + .verifyUploadAvatarApiCall() + .interceptDeleteAvatarRequest() + .clickChangeAvatarButton() + .clickDeleteAvatarButton() + .verifyDeleteAvatarApiCall(); + }); +}); diff --git a/cypress/fixtures/avatar.jpg b/cypress/fixtures/avatar.jpg new file mode 100644 index 0000000000000000000000000000000000000000..464ca73c65c86ab05e34b6294b3c63908f086469 GIT binary patch literal 48263 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­`1ML>mToyF3t>ISa_Zv8zMU->TAJx?(Fd`R(-b@W<8c@((y zg;=l!VolZOe1;DzXC#g-ELNJH#;@wdIbP+4JTeJjK;_9gzUe7G2z>#%@B%!>Zd^B} zuex9Dw4@KQ(9?A-1Fq4Na6?=3z^sztjuI7vrm<1(=XO)T*Q`*a+= z&6g=g0NIlmyOq+|qxmJ18YK%E=lf)ib3Z~C3%R6v)Nza-0)QTxIs$Uludrb;P$#W%c}t9FS!T^fb`a(S(F&J*!@piGJ5Y@t+fKX<*- z$yA*sK9OGSSs+WACz^6n)D9)hdZu&*znUcUD5=((c%Mt_+y)D#9Sl_CZhpT4Tz<0f zlZ2~|jqrzuMgZZ3L&TGg)aVc`a)2de6)3ynP;sHVIn>Za^Zn9%CQsZhX>UJ0uz3*b zb0dFkubusS=2cXvTTngg+-pyBOOPkqGnMvp7CT_Ft(P|&+&yG6%&EX+)&Dv1>!_bT znI@`azX3W$v))L%t8wI^Q1} zU;TcXgTPb@woy8URCTm1UY>R4x6yX|(W`TK(#+g0te;UP+>#x#nA;DcKliiOv@Rc$MT z$RSF&#x(lT$;ygpBMgKkgo%5NEPTz+GtRxG*3#SLG7lCjRmX*y)=-ZP2V~E22*>e= z=NSg?`8KB{Ev}nuzOE&22=5NjJdbC-b<6fwj+fa)X7TMuzqzHaZ>&zLj$RQub!3sA zf@^2B+Z>-`tyFn%czrJCP>;&E(;KwDI5s+&mur-k?`bVu)hoXMICz)r4^2$J;-}5w z%8kv zxe~(rJ*>nbcsKrc;l_`q_X1}ZPNbEyb#L4%Y%l$9A8(FOnz|-7$7)+|WUKk`3N90U z;?-=yN4Uv~CVCE`>efDIEQTQYu)tbZ;*d)oLgo$F#GX`3J5iN>YUR;NreayLXjEC$ zg0QZ}+z?+|g1}K}RqP^hN#h<_i^J=Na>ztQug^?vezFjnemljG8)WmK=0Hyxp6SF_ zx9ghXp{IvWv>3zlmyeNgvqf!L3v%k@I{?6UKU7

    suAzi8hOvb3)TbDIQgZf69IoE{ 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 PPcuaT@C8+6|C#+imTr!I literal 0 HcmV?d00001 diff --git a/cypress/fixtures/users.json b/cypress/fixtures/users.json index 1c3799e32ba..db27e9969f7 100644 --- a/cypress/fixtures/users.json +++ b/cypress/fixtures/users.json @@ -22,5 +22,9 @@ "devdoctor": { "username": "developdoctor", "password": "Test@123" + }, + "teststaff4": { + "username": "teststaff4", + "password": "Test@123" } } diff --git a/cypress/pageObject/Users/UserAvatar.ts b/cypress/pageObject/Users/UserAvatar.ts new file mode 100644 index 00000000000..ad8d59ef330 --- /dev/null +++ b/cypress/pageObject/Users/UserAvatar.ts @@ -0,0 +1,59 @@ +export class UserAvatar { + username: string; + constructor(username: string) { + this.username = username; + } + + navigateToProfile() { + cy.visit(`/users/${this.username}`); + return this; + } + + interceptUploadAvatarRequest() { + cy.intercept("POST", `/api/v1/users/${this.username}/profile_picture/`).as( + "uploadAvatar", + ); + return this; + } + + clickChangeAvatarButton() { + cy.verifyAndClickElement('[data-cy="change-avatar"]', "Change Avatar"); + return this; + } + + uploadAvatar() { + cy.get('input[title="changeFile"]').selectFile( + "cypress/fixtures/avatar.jpg", + { force: true }, + ); + return this; + } + + clickSaveAvatarButton() { + cy.verifyAndClickElement('[data-cy="save-cover-image"]', "Save"); + return this; + } + + verifyUploadAvatarApiCall() { + cy.wait("@uploadAvatar").its("response.statusCode").should("eq", 200); + return this; + } + + interceptDeleteAvatarRequest() { + cy.intercept( + "DELETE", + `/api/v1/users/${this.username}/profile_picture/`, + ).as("deleteAvatar"); + return this; + } + + clickDeleteAvatarButton() { + cy.verifyAndClickElement('[data-cy="delete-avatar"]', "Delete"); + return this; + } + + verifyDeleteAvatarApiCall() { + cy.wait("@deleteAvatar").its("response.statusCode").should("eq", 204); + return this; + } +} diff --git a/src/components/Common/AvatarEditModal.tsx b/src/components/Common/AvatarEditModal.tsx index 8ef7be45186..cbc45ef670a 100644 --- a/src/components/Common/AvatarEditModal.tsx +++ b/src/components/Common/AvatarEditModal.tsx @@ -349,6 +349,7 @@ const AvatarEditModal = ({ variant="destructive" onClick={deleteAvatar} disabled={isProcessing} + data-cy="delete-avatar" > {t("delete")} @@ -358,6 +359,7 @@ const AvatarEditModal = ({ variant="outline" onClick={uploadAvatar} disabled={isProcessing || !selectedFile} + data-cy="save-cover-image" > {isProcessing ? ( setEditAvatar(!editAvatar)} type="button" id="change-avatar" + data-cy="change-avatar" disabled > {t("change_avatar")} @@ -138,6 +139,7 @@ export default function UserAvatar({ username }: { username: string }) { onClick={() => setEditAvatar(!editAvatar)} type="button" id="change-avatar" + data-cy="change-avatar" > {t("change_avatar")} From f1150a88a3078d9b5ed38feda7db674ee2747cab Mon Sep 17 00:00:00 2001 From: Aditya Jindal Date: Thu, 27 Feb 2025 18:47:00 +0530 Subject: [PATCH 13/21] Fix: Inconsistent Feature Badge Styling Across Pages (#10839) --- src/common/constants.tsx | 28 ++++++++++----------- src/components/Facility/FacilityHome.tsx | 31 ++++++------------------ src/pages/Facility/Utils.tsx | 13 ++++++---- 3 files changed, 30 insertions(+), 42 deletions(-) diff --git a/src/common/constants.tsx b/src/common/constants.tsx index 8c8dd4731ea..c572e130f5b 100644 --- a/src/common/constants.tsx +++ b/src/common/constants.tsx @@ -123,37 +123,37 @@ export const FACILITY_FEATURE_TYPES: { id: 1, name: "CT Scan", icon: "l-compact-disc", - variant: "green", + variant: "blue", }, { id: 2, name: "Maternity Care", icon: "l-baby-carriage", - variant: "blue", + variant: "pink", }, { id: 3, name: "X-Ray", icon: "l-clipboard-alt", - variant: "amber", + variant: "blue", }, { id: 4, name: "Neonatal Care", icon: "l-baby-carriage", - variant: "teal", + variant: "pink", }, { id: 5, name: "Operation Theater", icon: "l-syringe", - variant: "red", + variant: "orange", }, { id: 6, name: "Blood Bank", icon: "l-medical-drip", - variant: "orange", + variant: "purple", }, { id: 7, @@ -165,17 +165,17 @@ export const FACILITY_FEATURE_TYPES: { id: 8, name: "Inpatient Services", icon: "l-hospital", - variant: "red", + variant: "orange", }, { id: 9, name: "Outpatient Services", icon: "l-hospital", - variant: "red", + variant: "indigo", }, { id: 10, - name: "Intensive Care Units", + name: "Intensive Care Units (ICU)", icon: "l-hospital", variant: "red", }, @@ -183,25 +183,25 @@ export const FACILITY_FEATURE_TYPES: { id: 11, name: "Pharmacy", icon: "l-hospital", - variant: "red", + variant: "indigo", }, { id: 12, name: "Rehabilitation Services", icon: "l-hospital", - variant: "red", + variant: "teal", }, { id: 13, name: "Home Care Services", icon: "l-hospital", - variant: "red", + variant: "teal", }, { id: 14, name: "Psychosocial Support", icon: "l-hospital", - variant: "red", + variant: "purple", }, { id: 15, @@ -213,7 +213,7 @@ export const FACILITY_FEATURE_TYPES: { id: 16, name: "Daycare Programs", icon: "l-hospital", - variant: "red", + variant: "yellow", }, ]; diff --git a/src/components/Facility/FacilityHome.tsx b/src/components/Facility/FacilityHome.tsx index c619575586c..c800605548b 100644 --- a/src/components/Facility/FacilityHome.tsx +++ b/src/components/Facility/FacilityHome.tsx @@ -8,7 +8,6 @@ import { toast } from "sonner"; import CareIcon from "@/CAREUI/icons/CareIcon"; -import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Markdown } from "@/components/ui/markdown"; @@ -27,6 +26,7 @@ import query from "@/Utils/request/query"; import uploadFile from "@/Utils/request/uploadFile"; import { getAuthorizationHeader } from "@/Utils/request/utils"; import { sleep } from "@/Utils/utils"; +import { FeatureBadge } from "@/pages/Facility/Utils"; import EditFacilitySheet from "@/pages/Organization/components/EditFacilitySheet"; import { FacilityData } from "@/types/facility/facility"; import type { @@ -362,33 +362,18 @@ export const FacilityHome = ({ facilityId }: Props) => { ) && ( - + {t("features")}

    - {facilityData?.features?.map( - (feature: number) => - FACILITY_FEATURE_TYPES.some( - (f) => f.id === feature, - ) && ( - - {getFacilityFeatureIcon(feature)} - - { - FACILITY_FEATURE_TYPES.find( - (f) => f.id === feature, - )?.name - } - - - ), - )} + {facilityData.features?.map((featureId) => ( + + ))}
    diff --git a/src/pages/Facility/Utils.tsx b/src/pages/Facility/Utils.tsx index f30ae8777c6..7de45b1ff17 100644 --- a/src/pages/Facility/Utils.tsx +++ b/src/pages/Facility/Utils.tsx @@ -12,11 +12,14 @@ export const FeatureBadge = ({ featureId }: { featureId: number }) => { return <>; } const variantStyles = { - green: "bg-green-100 text-green-800 hover:bg-green-100", - blue: "bg-blue-100 text-blue-800 hover:bg-blue-100", - amber: "bg-amber-100 text-amber-800 hover:bg-amber-100", - orange: "bg-orange-100 text-orange-800 hover:bg-orange-100", - teal: "bg-teal-100 text-teal-800 hover:bg-teal-100", + blue: "bg-blue-100 text-blue-800", + orange: "bg-orange-100 text-orange-800", + teal: "bg-teal-100 text-teal-800", + yellow: "bg-yellow-100 text-yellow-800", + pink: "bg-pink-100 text-pink-800", + red: "bg-red-100 text-red-800", + indigo: "bg-indigo-100 text-indigo-800", + purple: "bg-purple-100 text-purple-800", }; return ( From d9a1ecfbef806cc519cd7f01d350e820270a57dd Mon Sep 17 00:00:00 2001 From: Tanuj Nainwal <125687187+Tanuj1718@users.noreply.github.com> Date: Thu, 27 Feb 2025 18:49:02 +0530 Subject: [PATCH 14/21] fix: Improve responsiveness in Encounter medicine tab (#10767) --- .../Medicine/MedicationAdministration/AdministrationTab.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/Medicine/MedicationAdministration/AdministrationTab.tsx b/src/components/Medicine/MedicationAdministration/AdministrationTab.tsx index b70ec7e45e8..9e9b462af8b 100644 --- a/src/components/Medicine/MedicationAdministration/AdministrationTab.tsx +++ b/src/components/Medicine/MedicationAdministration/AdministrationTab.tsx @@ -770,7 +770,7 @@ export const AdministrationTab: React.FC = ({ return (
    -
    +
    @@ -794,7 +794,7 @@ export const AdministrationTab: React.FC = ({
    - {uploadedFiles && uploadedFiles.length > 1 && (
    {level > 0 && level < selectedLevels.length && ( From 0dba28a8824d02723a2b623bbf54dc4b8a53a824 Mon Sep 17 00:00:00 2001 From: G O Ashwin Praveen <143274955+ashwinpraveengo@users.noreply.github.com> Date: Thu, 27 Feb 2025 18:56:10 +0530 Subject: [PATCH 17/21] bugfix:fix Overapping greenindicator and scrollbar in Patient Details (#10802) --- src/components/Patient/PatientHome.tsx | 49 +++++++++++++------------- 1 file changed, 25 insertions(+), 24 deletions(-) diff --git a/src/components/Patient/PatientHome.tsx b/src/components/Patient/PatientHome.tsx index 69df69c4143..74e75e04fbd 100644 --- a/src/components/Patient/PatientHome.tsx +++ b/src/components/Patient/PatientHome.tsx @@ -108,34 +108,35 @@ export const PatientHome = (props: {
    -
    - {tabs.map((tab) => ( - - {t(tab.route)} - - ))} +
    +
    + {tabs.map((tab) => ( + + {t(tab.route)} + + ))} +
    -
    {Tab && ( From b6b9181dc3e28eb427a591481ebf4a313e1b10ea Mon Sep 17 00:00:00 2001 From: Mohammed Nihal <57055998+nihal467@users.noreply.github.com> Date: Thu, 27 Feb 2025 19:58:48 +0530 Subject: [PATCH 18/21] Modified the cypress viewport (#10851) --- cypress/e2e/patient_spec/patient_creation.cy.ts | 1 + cypress/e2e/patient_spec/patient_details.cy.ts | 1 + cypress/e2e/patient_spec/patient_encounter.cy.ts | 1 + 3 files changed, 3 insertions(+) diff --git a/cypress/e2e/patient_spec/patient_creation.cy.ts b/cypress/e2e/patient_spec/patient_creation.cy.ts index 3a8e8b77153..be0b1962115 100644 --- a/cypress/e2e/patient_spec/patient_creation.cy.ts +++ b/cypress/e2e/patient_spec/patient_creation.cy.ts @@ -101,6 +101,7 @@ describe("Patient Management", () => { ]; beforeEach(() => { + cy.viewport(1920, 1080); cy.loginByApi("doctor"); cy.visit("/"); }); diff --git a/cypress/e2e/patient_spec/patient_details.cy.ts b/cypress/e2e/patient_spec/patient_details.cy.ts index d96baf9bfef..dcbfe35accf 100644 --- a/cypress/e2e/patient_spec/patient_details.cy.ts +++ b/cypress/e2e/patient_spec/patient_details.cy.ts @@ -8,6 +8,7 @@ const patientDetails = new PatientDetails(); describe("Patient Management", () => { beforeEach(() => { + cy.viewport(1920, 1080); cy.loginByApi("devdoctor"); cy.visit("/"); }); diff --git a/cypress/e2e/patient_spec/patient_encounter.cy.ts b/cypress/e2e/patient_spec/patient_encounter.cy.ts index 2569b9567bc..7df0ff4866f 100644 --- a/cypress/e2e/patient_spec/patient_encounter.cy.ts +++ b/cypress/e2e/patient_spec/patient_encounter.cy.ts @@ -6,6 +6,7 @@ const patientEncounter = new PatientEncounter(); describe("Patient Encounter Questionnaire", () => { beforeEach(() => { + cy.viewport(1920, 1080); cy.loginByApi("devnurse"); cy.visit("/"); }); From 432c6143ba02ad12c5c6c228905f1318df4af59a Mon Sep 17 00:00:00 2001 From: Amjith Titus Date: Thu, 27 Feb 2025 20:23:03 +0530 Subject: [PATCH 19/21] Added Location sheet, removed Location Question (#10810) --- public/locale/en.json | 12 + .../Location/LocationHistorySheet.tsx | 49 -- src/components/Location/LocationSearch.tsx | 29 +- src/components/Location/LocationSheet.tsx | 460 ++++++++++++++++++ src/components/Patient/PatientInfoCard.tsx | 62 +-- .../QuestionTypes/LocationQuestion.tsx | 134 ----- .../QuestionTypes/QuestionInput.tsx | 17 - .../Questionnaire/QuestionnaireEditor.tsx | 1 - .../Questionnaire/data/StructuredFormData.tsx | 21 - .../Questionnaire/structured/handlers.ts | 35 -- .../Questionnaire/structured/types.ts | 6 - src/types/emr/encounter.ts | 9 +- src/types/location/association.ts | 27 +- src/types/location/locationApi.ts | 10 +- src/types/questionnaire/form.ts | 2 - src/types/questionnaire/question.ts | 3 +- 16 files changed, 547 insertions(+), 330 deletions(-) delete mode 100644 src/components/Location/LocationHistorySheet.tsx create mode 100644 src/components/Location/LocationSheet.tsx delete mode 100644 src/components/Questionnaire/QuestionTypes/LocationQuestion.tsx diff --git a/public/locale/en.json b/public/locale/en.json index e4f1cb71f5c..8d77bbb68eb 100644 --- a/public/locale/en.json +++ b/public/locale/en.json @@ -282,6 +282,7 @@ "active": "Active", "active_encounters": "Active Encounters", "active_files": "Active Files", + "active_location_cannot_be_in_future": "Active location cannot be in the future", "active_prescriptions": "Active Prescriptions", "add": "Add", "add_another_session": "Add another session", @@ -304,6 +305,7 @@ "add_location_description": "Create a Location such as Rooms/Beds", "add_new_beds": "Add New Bed(s)", "add_new_facility": "Add New Facility", + "add_new_location": "Add New Location", "add_new_patient": "Add New Patient", "add_new_user": "Add New User", "add_notes": "Add notes", @@ -689,6 +691,7 @@ "create_department_team_description": "Create a new department/team in this facility.", "create_encounter": "Create Encounter", "create_facility": "Create Facility", + "create_location_association": "Create Location Association", "create_new": "Create New", "create_new_asset": "Create New Asset", "create_new_encounter": "Create a new encounter to get started", @@ -1009,6 +1012,7 @@ "end_time": "End Time", "end_time_before_start_error": "End time cannot be before start time", "end_time_future_error": "End time cannot be in the future", + "end_time_required": "End time is required", "ended": "Ended", "enter_contact_value": "Enter contact value", "enter_department_team_description": "Enter department/team description (optional)", @@ -1354,6 +1358,8 @@ "local_ip_address_example": "e.g. 192.168.0.123", "location": "Location", "location_associated_successfully": "Location associated successfully", + "location_association_created_successfully": "Location association created successfully", + "location_association_updated_successfully": "Location association updated successfully", "location_beds_empty": "No beds available in this location", "location_created": "Location Created", "location_description": "Location Description", @@ -1406,6 +1412,7 @@ "manage_my_schedule": "Manage my schedule", "manage_organizations": "Manage Organizations", "manage_organizations_description": "Add or remove organizations from this questionnaire", + "manage_patient_location_and_transfers": "Manage patient location and transfers", "manage_prescriptions": "Manage Prescriptions", "manage_preset": "Manage preset {{ name }}", "manage_tags": "Manage Tags", @@ -1772,6 +1779,8 @@ "pincode_district_auto_fill_error": "Failed to auto-fill district information", "pincode_must_be_6_digits": "Pincode must be a 6-digit number", "pincode_state_auto_fill_error": "Failed to auto-fill state and district information", + "planned": "Planned", + "planned_reserved_cannot_be_in_past": "Planned/Reserved cannot be in the past", "play": "Play", "play_audio": "Play Audio", "please_assign_bed_to_patient": "Please assign a bed to this patient", @@ -1965,6 +1974,7 @@ "rescheduled": "Rescheduled", "rescheduling": "Rescheduling...", "resend_otp": "Resend OTP", + "reserved": "Reserved", "reset": "Reset", "reset_password": "Reset Password", "reset_password_note_self": "Enter your current password, then create and confirm your new password", @@ -2094,6 +2104,7 @@ "see_details": "See Details", "see_note": "See Note", "select": "Select", + "select_a_status": "Select a status", "select_a_value_set": "Select a Value Set", "select_additional_instructions": "Select additional instructions", "select_admit_source": "Select Admit Source", @@ -2242,6 +2253,7 @@ "start_time_before_authored_error": "Start time cannot be before the medication was prescribed", "start_time_future_error": "Start time cannot be in the future", "start_time_must_be_before_end_time": "Start time must be before end time", + "start_time_required": "Start time is required", "start_typing_to_search": "Start typing to search...", "state": "State", "state_reason_for_archiving": "State reason for archiving {{name}} file?", diff --git a/src/components/Location/LocationHistorySheet.tsx b/src/components/Location/LocationHistorySheet.tsx deleted file mode 100644 index de0ddb25abb..00000000000 --- a/src/components/Location/LocationHistorySheet.tsx +++ /dev/null @@ -1,49 +0,0 @@ -import { useTranslation } from "react-i18next"; - -import { ScrollArea } from "@/components/ui/scroll-area"; -import { - Sheet, - SheetContent, - SheetHeader, - SheetTitle, - SheetTrigger, -} from "@/components/ui/sheet"; - -import { LocationHistory } from "@/types/emr/encounter"; - -import { LocationTree } from "./LocationTree"; - -interface LocationHistorySheetProps { - trigger: React.ReactNode; - history: LocationHistory[]; -} - -export function LocationHistorySheet({ - trigger, - history, -}: LocationHistorySheetProps) { - const { t } = useTranslation(); - - return ( - - {trigger} - - - {t("location_history")} - - - {history.map((item, index) => ( -
    - -
    - ))} -
    -
    -
    - ); -} diff --git a/src/components/Location/LocationSearch.tsx b/src/components/Location/LocationSearch.tsx index 288a283773e..3e741c7c03f 100644 --- a/src/components/Location/LocationSearch.tsx +++ b/src/components/Location/LocationSearch.tsx @@ -1,6 +1,6 @@ import { useQuery } from "@tanstack/react-query"; +import { t } from "i18next"; import { useState } from "react"; -import { useTranslation } from "react-i18next"; import { Command, @@ -16,6 +16,7 @@ import { } from "@/components/ui/popover"; import query from "@/Utils/request/query"; +import { stringifyNestedObject } from "@/Utils/utils"; import { LocationList } from "@/types/location/location"; import locationApi from "@/types/location/locationApi"; @@ -34,7 +35,6 @@ export function LocationSearch({ disabled, value, }: LocationSearchProps) { - const { t } = useTranslation(); const [open, setOpen] = useState(false); const [search, setSearch] = useState(""); @@ -42,7 +42,7 @@ export function LocationSearch({ queryKey: ["locations", facilityId, mode, search], queryFn: query(locationApi.list, { pathParams: { facility_id: facilityId }, - queryParams: { mode, name: search }, + queryParams: { mode, name: search, form: "bd", available: "true" }, }), enabled: facilityId !== "preview", }); @@ -54,7 +54,7 @@ export function LocationSearch({ role="combobox" aria-expanded={open} > - {value?.name || "Select location..."} + {stringifyNestedObject(value || { name: "" }) || "Select location..."}
    @@ -65,7 +65,7 @@ export function LocationSearch({ className="outline-none border-none ring-0 shadow-none" onValueChange={setSearch} /> - No locations found. + {t("no_locations_found")} {locations?.results.map((location) => ( - {location.name} - - {t(`location_form__${location.form}`)} - {" in "} - {formatLocationParent(location)} - - - {t(`location_status__${location.status}`)} - + {stringifyNestedObject(location)} ))} @@ -93,12 +85,3 @@ export function LocationSearch({ ); } - -const formatLocationParent = (location: LocationList) => { - const parents: string[] = []; - while (location.parent?.name) { - parents.push(location.parent?.name); - location = location.parent; - } - return parents.reverse().join(" > "); -}; diff --git a/src/components/Location/LocationSheet.tsx b/src/components/Location/LocationSheet.tsx new file mode 100644 index 00000000000..96ceb06b45b --- /dev/null +++ b/src/components/Location/LocationSheet.tsx @@ -0,0 +1,460 @@ +import { useMutation, useQueryClient } from "@tanstack/react-query"; +import { format, isAfter, isBefore, parseISO } from "date-fns"; +import { useEffect, useState } from "react"; +import { useTranslation } from "react-i18next"; +import { toast } from "sonner"; + +import { Badge } from "@/components/ui/badge"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { ScrollArea } from "@/components/ui/scroll-area"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { + Sheet, + SheetContent, + SheetHeader, + SheetTitle, + SheetTrigger, +} from "@/components/ui/sheet"; + +import mutate from "@/Utils/request/mutate"; +import { stringifyNestedObject } from "@/Utils/utils"; +import { LocationHistory } from "@/types/emr/encounter"; +import { + LocationAssociationStatus, + LocationAssociationUpdate, +} from "@/types/location/association"; +import { LocationList } from "@/types/location/location"; +import locationApi from "@/types/location/locationApi"; + +import { LocationSearch } from "./LocationSearch"; +import { LocationTree } from "./LocationTree"; + +interface LocationSheetProps { + trigger: React.ReactNode; + history: LocationHistory[]; + facilityId: string; + encounterId: string; +} + +interface LocationState extends LocationHistory { + displayStatus: LocationAssociationStatus; +} + +interface ValidationError { + message: string; + field: "start_datetime" | "end_datetime"; +} + +// Omit id field for creation +type LocationAssociationCreate = Omit; + +export function LocationSheet({ + trigger, + history, + facilityId, + encounterId, +}: LocationSheetProps) { + const { t } = useTranslation(); + const queryClient = useQueryClient(); + + const initialState = { + location: "", + status: "active", + start_datetime: format(new Date(), "yyyy-MM-dd'T'HH:mm"), + end_datetime: format(new Date(), "yyyy-MM-dd'T'HH:mm"), + encounter: encounterId, + }; + const [newLocation, setNewLocation] = useState(initialState); + + const [locations, setLocations] = useState([]); + + useEffect(() => { + setLocations( + history.map((loc) => ({ + ...loc, + displayStatus: loc.status, + end_datetime: loc.status === "active" ? undefined : loc.end_datetime, + })), + ); + }, [history]); + + function validateTimes( + status: LocationAssociationStatus, + startTime: string, + endTime?: string, + ): ValidationError | null { + const now = new Date(); + const start = parseISO(startTime); + + if (!startTime) { + return { message: t("start_time_required"), field: "start_datetime" }; + } + + if (status !== "active" && !endTime) { + return { message: t("end_time_required"), field: "end_datetime" }; + } + + if (endTime) { + const end = parseISO(endTime); + if (isBefore(end, start)) { + return { + message: t("start_time_must_be_before_end_time"), + field: "end_datetime", + }; + } + } + + if ( + (status === "planned" || status === "reserved") && + isBefore(start, now) + ) { + return { + message: t("planned_reserved_cannot_be_in_past"), + field: "start_datetime", + }; + } + + if (status === "active" && isAfter(start, now)) { + return { + message: t("active_location_cannot_be_in_future"), + field: "start_datetime", + }; + } + + return null; + } + + const handleLocationUpdate = (updatedLocation: LocationState) => { + setLocations((prevLocations) => + prevLocations.map((loc) => + loc.id === updatedLocation.id + ? { + ...updatedLocation, + end_datetime: + updatedLocation.status === "active" + ? undefined + : updatedLocation.end_datetime, + } + : loc, + ), + ); + }; + + const [selectedLocation, setSelectedLocation] = useState( + null, + ); + + const updateAssociation = useMutation({ + mutationFn: (location: LocationAssociationUpdate) => { + const validationError = validateTimes( + location.status, + location.start_datetime, + location.end_datetime, + ); + + if (validationError) { + throw new Error(validationError.message); + } + + return mutate(locationApi.updateAssociation, { + pathParams: { + facility_external_id: facilityId, + location_external_id: location.location, + external_id: location.id, + }, + })(location); + }, + onSuccess: () => { + toast.success(t("location_association_updated_successfully")); + queryClient.invalidateQueries({ queryKey: ["encounter", encounterId] }); + }, + }); + + const { mutate: createAssociation, isPending } = useMutation({ + mutationFn: (data: LocationAssociationCreate) => { + const validationError = validateTimes( + data.status, + data.start_datetime, + data.end_datetime, + ); + + if (validationError) { + throw new Error(validationError.message); + } + + return mutate(locationApi.createAssociation, { + pathParams: { + facility_external_id: facilityId, + location_external_id: selectedLocation?.id, + }, + })(data); + }, + onSuccess: () => { + toast.success(t("location_association_created_successfully")); + queryClient.invalidateQueries({ queryKey: ["encounter", encounterId] }); + setNewLocation(initialState); + setSelectedLocation(null); + }, + }); + + const renderLocation = (location: LocationState) => ( +
    +
    + +
    + + +
    +
    + + {stringifyNestedObject(location.location, " < ")} + +
    + {(location.status === "active" || + location.status === "planned" || + location.status === "reserved") && ( +
    + + + handleLocationUpdate({ + ...location, + start_datetime: e.target.value, + }) + } + className="h-9 w-auto" + /> +
    + )} + {location.status !== "active" && ( +
    + + + handleLocationUpdate({ + ...location, + end_datetime: e.target.value, + }) + } + className="h-9" + /> +
    + )} +
    +
    + ); + + // Get locations by their original display status + const activeLocation = locations.find( + (loc) => loc.displayStatus === "active", + ); + const plannedLocations = locations.filter( + (loc) => loc.displayStatus === "planned", + ); + const reservedLocations = locations.filter( + (loc) => loc.displayStatus === "reserved", + ); + + return ( + + {trigger} + + + + {t("update_location")} + +

    + {t("manage_patient_location_and_transfers")} +

    +
    + +
    + {/* Active Location */} + {activeLocation && renderLocation(activeLocation)} + + {/* Reserved Locations */} + {reservedLocations.map((location) => renderLocation(location))} + + {/* Planned Locations */} + {plannedLocations.map((location) => renderLocation(location))} + +
    +
    +
    + +
    +
    + setSelectedLocation(location)} + value={selectedLocation} + /> + {selectedLocation && ( +
    +
    + + +
    + {(newLocation.status === "active" || + newLocation.status === "planned" || + newLocation.status === "reserved") && ( +
    + + + setNewLocation((prev) => ({ + ...prev, + start_datetime: e.target.value, + })) + } + className="h-9" + /> +
    + )} + {newLocation.status !== "active" && ( +
    + + + setNewLocation((prev) => ({ + ...prev, + end_datetime: e.target.value, + })) + } + className="h-9" + /> +
    + )} + +
    + )} +
    +
    + {history.map((item, index) => ( +
    + +
    + ))} +
    + + + + ); +} diff --git a/src/components/Patient/PatientInfoCard.tsx b/src/components/Patient/PatientInfoCard.tsx index 1e9b2922126..ff54771c178 100644 --- a/src/components/Patient/PatientInfoCard.tsx +++ b/src/components/Patient/PatientInfoCard.tsx @@ -43,7 +43,7 @@ import { } from "@/components/ui/popover"; import { Avatar } from "@/components/Common/Avatar"; -import { LocationHistorySheet } from "@/components/Location/LocationHistorySheet"; +import { LocationSheet } from "@/components/Location/LocationSheet"; import { LocationTree } from "@/components/Location/LocationTree"; import LinkDepartmentsSheet from "@/components/Patient/LinkDepartmentsSheet"; @@ -51,7 +51,11 @@ import { PLUGIN_Component } from "@/PluginEngine"; import routes from "@/Utils/request/api"; import mutate from "@/Utils/request/mutate"; import { formatDateTime, formatPatientAge } from "@/Utils/utils"; -import { Encounter, completedEncounterStatus } from "@/types/emr/encounter"; +import { + Encounter, + completedEncounterStatus, + inactiveEncounterStatus, +} from "@/types/emr/encounter"; import { Patient } from "@/types/emr/newPatient"; import { FacilityOrganization } from "@/types/facilityOrganization/facilityOrganization"; @@ -372,7 +376,9 @@ export default function PatientInfoCard(props: PatientInfoCardProps) { {t("location")}

    - @@ -394,39 +400,40 @@ export default function PatientInfoCard(props: PatientInfoCardProps) { - {!disableButtons && ( - <> -
    +
    + - - {t("update_location")} - + {t("update_location")} - - )} + } + history={encounter.location_history} + />
    ) : ( - encounter.status !== "completed" && - !disableButtons && ( + !inactiveEncounterStatus.includes(encounter.status) && ( - - - {t("add_location")} - + + + {t("add_location")} +
    + } + history={encounter.location_history} + /> ) )} @@ -522,8 +529,7 @@ export default function PatientInfoCard(props: PatientInfoCardProps) { void; - disabled?: boolean; - facilityId: string; - locationId: string; - encounterId: string; -} - -export function LocationQuestion({ - questionnaireResponse, - updateQuestionnaireResponseCB, - disabled, - facilityId, - encounterId, -}: LocationQuestionProps) { - const { data: encounter } = useQuery({ - queryKey: ["encounter", encounterId], - queryFn: query(api.encounter.get, { - pathParams: { id: encounterId }, - queryParams: { facility: facilityId }, - }), - }); - - const [selectedLocation, setSelectedLocation] = useState( - null, - ); - - useEffect(() => { - if (encounter?.current_location) { - setSelectedLocation(encounter.current_location); - } - }, [encounter]); - - const values = - (questionnaireResponse.values?.[0] - ?.value as unknown as LocationAssociationQuestion[]) || []; - - const association = values[0] ?? {}; - - const handleUpdateAssociation = ( - updates: Partial, - ) => { - const newAssociation: LocationAssociationQuestion = { - id: association?.id || null, - encounter: encounterId, - start_datetime: association?.start_datetime || new Date().toISOString(), - end_datetime: null, - status: "active", - location: association?.location || "", - meta: {}, - created_by: null, - updated_by: null, - ...updates, - }; - - updateQuestionnaireResponseCB( - [{ type: "location_association", value: [newAssociation] }], - questionnaireResponse.question_id, - ); - }; - - const handleLocationSelect = (location: LocationList) => { - setSelectedLocation(location); - handleUpdateAssociation({ location: location.id }); - }; - - return ( -
    -
    -
    - - -
    - - {selectedLocation && ( -
    - - - handleUpdateAssociation({ - start_datetime: new Date(e.target.value).toISOString(), - }) - } - disabled={disabled} - className="h-9" - /> -
    - )} -
    -
    - ); -} diff --git a/src/components/Questionnaire/QuestionTypes/QuestionInput.tsx b/src/components/Questionnaire/QuestionTypes/QuestionInput.tsx index 25763d3d41a..f23073636d9 100644 --- a/src/components/Questionnaire/QuestionTypes/QuestionInput.tsx +++ b/src/components/Questionnaire/QuestionTypes/QuestionInput.tsx @@ -20,7 +20,6 @@ import { ChoiceQuestion } from "./ChoiceQuestion"; import { DateTimeQuestion } from "./DateTimeQuestion"; import { DiagnosisQuestion } from "./DiagnosisQuestion"; import { EncounterQuestion } from "./EncounterQuestion"; -import { LocationQuestion } from "./LocationQuestion"; import { MedicationRequestQuestion } from "./MedicationRequestQuestion"; import { MedicationStatementQuestion } from "./MedicationStatementQuestion"; import { NotesInput } from "./NotesInput"; @@ -186,22 +185,6 @@ export function QuestionInput({ return ( Create an encounter first in order to update it ); - case "location_association": - if (encounterId) { - return ( - - ); - } - return ( - - Location cannot be recorded without an active encounter - - ); } return null; diff --git a/src/components/Questionnaire/QuestionnaireEditor.tsx b/src/components/Questionnaire/QuestionnaireEditor.tsx index daf1c189f70..82e631ffe1c 100644 --- a/src/components/Questionnaire/QuestionnaireEditor.tsx +++ b/src/components/Questionnaire/QuestionnaireEditor.tsx @@ -101,7 +101,6 @@ const STRUCTURED_QUESTION_TYPES = [ { value: "diagnosis", label: "Diagnosis" }, { value: "encounter", label: "Encounter" }, { value: "appointment", label: "Appointment" }, - { value: "location_association", label: "Location Association" }, ] as const; interface Organization { diff --git a/src/components/Questionnaire/data/StructuredFormData.tsx b/src/components/Questionnaire/data/StructuredFormData.tsx index 78d10fcb982..918d2a1561f 100644 --- a/src/components/Questionnaire/data/StructuredFormData.tsx +++ b/src/components/Questionnaire/data/StructuredFormData.tsx @@ -120,26 +120,6 @@ const symptom_questionnaire: QuestionnaireDetail = { tags: [], }; -const location_association_questionnaire: QuestionnaireDetail = { - id: "location_association", - slug: "location_association", - version: "0.0.1", - title: "Location Association", - status: "active", - subject_type: "patient", - questions: [ - { - id: "location_association", - text: "Location Association", - type: "structured", - structured_type: "location_association", - link_id: "1.1", - required: true, - }, - ], - tags: [], -}; - export const FIXED_QUESTIONNAIRES: Record = { encounter: encounterQuestionnaire, medication_request: medication_request_questionnaire, @@ -147,5 +127,4 @@ export const FIXED_QUESTIONNAIRES: Record = { medication_statement: medication_statement_questionnaire, diagnosis: diagnosis_questionnaire, symptom: symptom_questionnaire, - location_association: location_association_questionnaire, }; diff --git a/src/components/Questionnaire/structured/handlers.ts b/src/components/Questionnaire/structured/handlers.ts index 47b020347f9..95995bca0f2 100644 --- a/src/components/Questionnaire/structured/handlers.ts +++ b/src/components/Questionnaire/structured/handlers.ts @@ -3,8 +3,6 @@ import { RequestTypeFor, } from "@/components/Questionnaire/structured/types"; -import { LocationAssociationQuestion } from "@/types/location/association"; -import locationApi from "@/types/location/locationApi"; import { StructuredQuestionType } from "@/types/questionnaire/question"; interface StructuredHandlerContext { @@ -159,39 +157,6 @@ export const structuredHandlers: { ]; }, }, - location_association: { - getRequests: ( - locationAssociations: LocationAssociationQuestion[], - { facilityId, encounterId }, - ) => { - if (!locationAssociations.length) { - return []; - } - - if (!facilityId) { - throw new Error( - "Cannot create location association without a facility", - ); - } - - return locationAssociations.map((locationAssociation) => { - return { - url: locationApi.createAssociation.path - .replace("{facility_external_id}", facilityId) - .replace("{location_external_id}", locationAssociation.location), - method: locationApi.createAssociation.method, - body: { - encounter: encounterId, - start_datetime: locationAssociation.start_datetime, - end_datetime: locationAssociation.end_datetime, - status: locationAssociation.status, - meta: locationAssociation.meta, - }, - reference_id: `location_association_${locationAssociation}`, - }; - }); - }, - }, }; export const getStructuredRequests = ( diff --git a/src/components/Questionnaire/structured/types.ts b/src/components/Questionnaire/structured/types.ts index 03bc5f87a5b..f8e88017c6a 100644 --- a/src/components/Questionnaire/structured/types.ts +++ b/src/components/Questionnaire/structured/types.ts @@ -4,10 +4,6 @@ import { EncounterEditRequest } from "@/types/emr/encounter"; import { MedicationRequest } from "@/types/emr/medicationRequest"; import { MedicationStatementRequest } from "@/types/emr/medicationStatement"; import { SymptomRequest } from "@/types/emr/symptom/symptom"; -import { - LocationAssociationQuestion, - LocationAssociationWrite, -} from "@/types/location/association"; import { StructuredQuestionType } from "@/types/questionnaire/question"; import { AppointmentCreateRequest, @@ -23,7 +19,6 @@ export interface StructuredDataMap { medication_statement: MedicationStatementRequest; encounter: EncounterEditRequest; appointment: CreateAppointmentQuestion; - location_association: LocationAssociationQuestion; } // Map structured types to their request types @@ -35,7 +30,6 @@ export interface StructuredRequestMap { medication_statement: { datapoints: MedicationStatementRequest[] }; encounter: EncounterEditRequest; appointment: AppointmentCreateRequest; - location_association: LocationAssociationWrite; } export type RequestTypeFor = diff --git a/src/types/emr/encounter.ts b/src/types/emr/encounter.ts index 45c6602b763..a5175710689 100644 --- a/src/types/emr/encounter.ts +++ b/src/types/emr/encounter.ts @@ -1,5 +1,6 @@ import { Patient } from "@/types/emr/newPatient"; import { FacilityOrganization } from "@/types/facilityOrganization/facilityOrganization"; +import { LocationAssociationStatus } from "@/types/location/association"; import { LocationList } from "@/types/location/location"; import { UserBase } from "@/types/user/user"; @@ -118,9 +119,11 @@ export type StatusHistory = { }; export type LocationHistory = { + id: string; start_datetime: string; location: LocationList; - status: string; + status: LocationAssociationStatus; + end_datetime?: string; }; export interface Encounter { @@ -172,3 +175,7 @@ export interface EncounterRequest { } export const completedEncounterStatus = ["completed", "discharged"]; +export const inactiveEncounterStatus = [ + ...["cancelled", "entered_in_error", "discontinued"], + ...(completedEncounterStatus as EncounterStatus[]), +] as const; diff --git a/src/types/location/association.ts b/src/types/location/association.ts index 4397a665a25..238cd316a7e 100644 --- a/src/types/location/association.ts +++ b/src/types/location/association.ts @@ -1,22 +1,33 @@ +export const LOCATION_ASSOCIATION_STATUSES = [ + "planned", + "active", + "reserved", + "completed", +] as const; + +export type LocationAssociationStatus = + (typeof LOCATION_ASSOCIATION_STATUSES)[number]; + export interface LocationAssociation { meta: Record; id: string | null; encounter: string; start_datetime: string; end_datetime: string | null; - status: string; + status: LocationAssociationStatus; created_by: string | null; updated_by: string | null; } -export interface LocationAssociationQuestion extends LocationAssociation { +export interface LocationAssociationRequest { + meta?: Record; + encounter: string; + start_datetime: string; + end_datetime?: string; + status: LocationAssociationStatus; location: string; } -export interface LocationAssociationWrite { - encounter: string; - start_datetime: string; - end_datetime?: string | null; - status: string; - meta?: Record; +export interface LocationAssociationUpdate extends LocationAssociationRequest { + id: string; } diff --git a/src/types/location/locationApi.ts b/src/types/location/locationApi.ts index d58b6392bf6..b9cceeda4cc 100644 --- a/src/types/location/locationApi.ts +++ b/src/types/location/locationApi.ts @@ -2,7 +2,11 @@ import { HttpMethod, Type } from "@/Utils/request/api"; import { PaginatedResponse } from "@/Utils/request/types"; import { FacilityOrganization } from "@/types/facilityOrganization/facilityOrganization"; -import { LocationAssociation, LocationAssociationWrite } from "./association"; +import { + LocationAssociation, + LocationAssociationRequest, + LocationAssociationUpdate, +} from "./association"; import { LocationDetail, LocationList, LocationWrite } from "./location"; export default { @@ -54,7 +58,7 @@ export default { path: "/api/v1/facility/{facility_external_id}/location/{location_external_id}/association/", method: HttpMethod.POST, TRes: Type(), - TBody: Type(), + TBody: Type(), }, getAssociation: { path: "/api/v1/facility/{facility_external_id}/location/{location_external_id}/association/{external_id}/", @@ -65,7 +69,7 @@ export default { path: "/api/v1/facility/{facility_external_id}/location/{location_external_id}/association/{external_id}/", method: HttpMethod.PUT, TRes: Type(), - TBody: Type(), + TBody: Type(), }, deleteAssociation: { path: "/api/v1/facility/{facility_external_id}/location/{location_external_id}/association/{external_id}/", diff --git a/src/types/questionnaire/form.ts b/src/types/questionnaire/form.ts index 07cec713f51..4a932eb384b 100644 --- a/src/types/questionnaire/form.ts +++ b/src/types/questionnaire/form.ts @@ -4,7 +4,6 @@ import { EncounterEditRequest } from "@/types/emr/encounter"; import { MedicationRequest } from "@/types/emr/medicationRequest"; import { MedicationStatementRequest } from "@/types/emr/medicationStatement"; import { SymptomRequest } from "@/types/emr/symptom/symptom"; -import { LocationAssociationQuestion } from "@/types/location/association"; import { Code } from "@/types/questionnaire/code"; import { Quantity } from "@/types/questionnaire/quantity"; import { StructuredQuestionType } from "@/types/questionnaire/question"; @@ -28,7 +27,6 @@ export type ResponseValue = | RV<"allergy_intolerance", AllergyIntoleranceRequest[]> | RV<"medication_request", MedicationRequest[]> | RV<"medication_statement", MedicationStatementRequest[]> - | RV<"location_association", LocationAssociationQuestion[]> | RV<"symptom", SymptomRequest[]> | RV<"diagnosis", DiagnosisRequest[]> | RV<"encounter", EncounterEditRequest[]> diff --git a/src/types/questionnaire/question.ts b/src/types/questionnaire/question.ts index f0199d63890..d99d1c46237 100644 --- a/src/types/questionnaire/question.ts +++ b/src/types/questionnaire/question.ts @@ -40,8 +40,7 @@ export type StructuredQuestionType = | "symptom" | "diagnosis" | "encounter" - | "appointment" - | "location_association"; + | "appointment"; type EnableWhenNumeric = { operator: "greater" | "less" | "greater_or_equals" | "less_or_equals"; From cf1292b39d7301cf9551cdb94fbac810fac4192a Mon Sep 17 00:00:00 2001 From: Jeffrin Jojo <135723871+Jeffrin2005@users.noreply.github.com> Date: Thu, 27 Feb 2025 21:58:36 +0530 Subject: [PATCH 20/21] Fix : Alignment of Country List in Phone Number Search (#10778) Co-authored-by: Rithvik Nishad --- src/components/ui/phone-input.tsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/components/ui/phone-input.tsx b/src/components/ui/phone-input.tsx index ba99fdcf042..38813cfc1d4 100644 --- a/src/components/ui/phone-input.tsx +++ b/src/components/ui/phone-input.tsx @@ -120,7 +120,11 @@ const CountrySelect = ({ /> - + Date: Fri, 28 Feb 2025 07:09:59 +0530 Subject: [PATCH 21/21] Ensure DialogContent Accessibility by Adding Description Support (#10635) --- public/locale/en.json | 5 ++++- src/components/Common/AvatarEditModal.tsx | 4 ++++ src/components/ui/sidebar.tsx | 12 +++++++++++- src/pages/Encounters/tabs/EncounterNotesTab.tsx | 11 ++++++++++- .../components/CreateScheduleTemplateSheet.tsx | 4 ++++ .../components/EditScheduleTemplateSheet.tsx | 4 ++++ 6 files changed, 37 insertions(+), 3 deletions(-) diff --git a/public/locale/en.json b/public/locale/en.json index 8d77bbb68eb..941485216fc 100644 --- a/public/locale/en.json +++ b/public/locale/en.json @@ -953,6 +953,7 @@ "encounter_manage_organization_description": "Add or remove organizations from this encouter", "encounter_marked_as_complete": "Encounter Completed", "encounter_notes__all_discussions": "All Discussions", + "encounter_notes__all_discussions_description": "View and manage encounternotes discussion threads", "encounter_notes__be_first_to_send": "Be the first to send a message", "encounter_notes__choose_template": "Choose a template or enter a custom title", "encounter_notes__create_discussion": "Create a new discussion thread to organize your conversation topics.", @@ -2145,7 +2146,6 @@ "select_policy_to_add_items": "Select a Policy to Add Items", "select_practitioner": "Select Practitioner", "select_previous": "Select Previous Fields", - "show_on_map": "Show on Map", "select_priority": "Select Priority", "select_prn_reason": "Select reason for PRN", "select_register_patient": "Select/Register Patient", @@ -2212,10 +2212,13 @@ "show_all_notifications": "Show All", "show_all_slots": "Show all slots", "show_default_presets": "Show Default Presets", + "show_on_map": "Show on Map", "show_patient_presets": "Show Patient Presets", "show_unread_notifications": "Show Unread", "showing_all_appointments": "Showing all appointments", "showing_x_of_y": "Showing {{x}} of {{y}}", + "sidebar": "sidebar", + "sidebar_description": "sidebar provides navigation to different sections", "sign_in": "Sign in", "sign_out": "Sign out", "site": "Site", diff --git a/src/components/Common/AvatarEditModal.tsx b/src/components/Common/AvatarEditModal.tsx index cbc45ef670a..9499f241e46 100644 --- a/src/components/Common/AvatarEditModal.tsx +++ b/src/components/Common/AvatarEditModal.tsx @@ -16,6 +16,7 @@ import { Button } from "@/components/ui/button"; import { Dialog, DialogContent, + DialogDescription, DialogHeader, DialogTitle, } from "@/components/ui/dialog"; @@ -224,6 +225,9 @@ const AvatarEditModal = ({ {title} + + {t("edit_avatar")} +
    diff --git a/src/components/ui/sidebar.tsx b/src/components/ui/sidebar.tsx index 13d8ab1bb7f..727cb808a41 100644 --- a/src/components/ui/sidebar.tsx +++ b/src/components/ui/sidebar.tsx @@ -3,6 +3,7 @@ import { VisuallyHidden } from "@radix-ui/react-visually-hidden"; import { VariantProps, cva } from "class-variance-authority"; import { PanelLeftClose, PanelRightClose } from "lucide-react"; import * as React from "react"; +import { useTranslation } from "react-i18next"; import useKeyboardShortcut from "use-keyboard-shortcut"; import { cn } from "@/lib/utils"; @@ -10,7 +11,12 @@ import { cn } from "@/lib/utils"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Separator } from "@/components/ui/separator"; -import { Sheet, SheetContent, SheetTitle } from "@/components/ui/sheet"; +import { + Sheet, + SheetContent, + SheetDescription, + SheetTitle, +} from "@/components/ui/sheet"; import { Skeleton } from "@/components/ui/skeleton"; import { Tooltip, @@ -178,6 +184,7 @@ const Sidebar = React.forwardRef< ref, ) => { const { isMobile, state, openMobile, setOpenMobile } = useSidebar(); + const { t } = useTranslation(); if (collapsible === "none") { return ( @@ -197,6 +204,9 @@ const Sidebar = React.forwardRef< if (isMobile) { return ( + + {t("sidebar_description")} + { {/* Mobile Sheet */} + + {t("encounter_notes__all_discussions_description")} + + {t("encounter")}
    diff --git a/src/pages/Scheduling/components/CreateScheduleTemplateSheet.tsx b/src/pages/Scheduling/components/CreateScheduleTemplateSheet.tsx index daee952132e..94289af35cb 100644 --- a/src/pages/Scheduling/components/CreateScheduleTemplateSheet.tsx +++ b/src/pages/Scheduling/components/CreateScheduleTemplateSheet.tsx @@ -30,6 +30,7 @@ import { Sheet, SheetClose, SheetContent, + SheetDescription, SheetFooter, SheetHeader, SheetTitle, @@ -252,6 +253,9 @@ export default function CreateScheduleTemplateSheet({ {t("create_schedule_template")} + + {t("create_schedule_template")} +
    diff --git a/src/pages/Scheduling/components/EditScheduleTemplateSheet.tsx b/src/pages/Scheduling/components/EditScheduleTemplateSheet.tsx index b1d7b2f25a6..51486057c7c 100644 --- a/src/pages/Scheduling/components/EditScheduleTemplateSheet.tsx +++ b/src/pages/Scheduling/components/EditScheduleTemplateSheet.tsx @@ -43,6 +43,7 @@ import { Input } from "@/components/ui/input"; import { Sheet, SheetContent, + SheetDescription, SheetHeader, SheetTitle, SheetTrigger, @@ -88,6 +89,9 @@ export default function EditScheduleTemplateSheet({ {t("edit_schedule_template")} + + {t("edit_schedule_template")} +