diff --git a/src/Routers/AppRouter.tsx b/src/Routers/AppRouter.tsx index 528f090dd6c..a49ca7767f9 100644 --- a/src/Routers/AppRouter.tsx +++ b/src/Routers/AppRouter.tsx @@ -19,6 +19,7 @@ import FacilityRoutes from "@/Routers/routes/FacilityRoutes"; import PatientRoutes from "@/Routers/routes/PatientRoutes"; import ResourceRoutes from "@/Routers/routes/ResourceRoutes"; import UserRoutes from "@/Routers/routes/UserRoutes"; +import { PermissionProvider } from "@/context/PermissionContext"; import { PlugConfigEdit } from "@/pages/Apps/PlugConfigEdit"; import { PlugConfigList } from "@/pages/Apps/PlugConfigList"; import UserDashboard from "@/pages/UserDashboard"; @@ -87,32 +88,37 @@ export default function AppRouter() { return ( - {shouldShowSidebar && } -
-
-
- {shouldShowSidebar && } -
- - care logo - -
-
} +
- }> - {pages} - -
-
+
+
+ {shouldShowSidebar && } +
+ + care logo + +
+
+ }> + {pages} + +
+ +
); } diff --git a/src/components/Users/models.tsx b/src/components/Users/models.tsx index a26b9531feb..4ea4b720fde 100644 --- a/src/components/Users/models.tsx +++ b/src/components/Users/models.tsx @@ -2,7 +2,6 @@ import { GENDER_TYPES, UserRole } from "@/common/constants"; import { FeatureFlag } from "@/Utils/featureFlags"; import { Organization } from "@/types/organization/organization"; -import { Permission } from "@/types/permission/permission"; interface HomeFacilityObjectModel { id?: string; @@ -56,7 +55,7 @@ export type UserModel = UserBareMinimum & { user_flags?: FeatureFlag[]; facilities?: UserFacilityModel[]; organizations?: Organization[]; - permissions: Permission[]; + permissions: string[]; }; export type UserBaseModel = { diff --git a/src/context/PermissionContext.tsx b/src/context/PermissionContext.tsx new file mode 100644 index 00000000000..ee732cca2c4 --- /dev/null +++ b/src/context/PermissionContext.tsx @@ -0,0 +1,73 @@ +import { createContext, useContext, useMemo } from "react"; + +interface PermissionContextType { + /** + * Check if user has a specific permission in either their user permissions or object permissions + * @param permission - The permission to check + * @param objectPermissions - Optional object-specific permissions + */ + hasPermission: (permission: string, objectPermissions?: string[]) => boolean; + /** + * Raw permissions array from the user + */ + userPermissions: string[]; + /** + * Whether the user is a super admin + */ + isSuperAdmin: boolean; +} + +const PermissionContext = createContext(null); + +interface PermissionProviderProps { + children: React.ReactNode; + userPermissions: string[]; + isSuperAdmin?: boolean; +} + +export function PermissionProvider({ + children, + userPermissions, + isSuperAdmin = false, +}: PermissionProviderProps) { + const value = useMemo(() => { + const hasPermission = ( + permission: string, + objectPermissions?: string[], + ) => { + if (isSuperAdmin) return true; + return ( + userPermissions.includes(permission) || + (objectPermissions?.includes(permission) ?? false) + ); + }; + + return { + hasPermission, + userPermissions, + isSuperAdmin, + }; + }, [userPermissions, isSuperAdmin]); + + return ( + + {children} + + ); +} + +export function usePermissions() { + const context = useContext(PermissionContext); + if (!context) { + throw new Error("usePermissions must be used within a PermissionProvider"); + } + return context; +} + +export function useHasPermission( + permission: string, + objectPermissions?: string[], +) { + const { hasPermission } = usePermissions(); + return hasPermission(permission, objectPermissions); +} diff --git a/src/pages/Organization/OrganizationFacilities.tsx b/src/pages/Organization/OrganizationFacilities.tsx index 0b02ae09fee..0717e682921 100644 --- a/src/pages/Organization/OrganizationFacilities.tsx +++ b/src/pages/Organization/OrganizationFacilities.tsx @@ -37,7 +37,7 @@ export default function OrganizationFacilities({ page: qParams.page, limit: resultsPerPage, offset: (qParams.page - 1) * resultsPerPage, - geo_organization: id, + organization: id, name: qParams.name, ...advancedFilter.filter, }, diff --git a/src/pages/Organization/OrganizationPatients.tsx b/src/pages/Organization/OrganizationPatients.tsx index 05dcdd15901..c31c9652b23 100644 --- a/src/pages/Organization/OrganizationPatients.tsx +++ b/src/pages/Organization/OrganizationPatients.tsx @@ -1,5 +1,6 @@ import { useQuery } from "@tanstack/react-query"; import { Link } from "raviger"; +import { useState } from "react"; import RecordMeta from "@/CAREUI/display/RecordMeta"; import CareIcon from "@/CAREUI/icons/CareIcon"; @@ -14,6 +15,7 @@ import useFilters from "@/hooks/useFilters"; import query from "@/Utils/request/query"; import { Patient } from "@/types/emr/newPatient"; +import { Organization } from "@/types/organization/organization"; import organizationApi from "@/types/organization/organizationApi"; import OrganizationLayout from "./components/OrganizationLayout"; @@ -26,20 +28,21 @@ interface Props { export default function OrganizationPatients({ id, navOrganizationId }: Props) { const { qParams, Pagination, advancedFilter, resultsPerPage, updateQuery } = useFilters({ limit: 14, cacheBlacklist: ["patient"] }); + const [organization, setOrganization] = useState(null); const { data: patients, isLoading } = useQuery({ queryKey: ["organizationPatients", id, qParams], queryFn: query(organizationApi.listPatients, { pathParams: { id }, queryParams: { - geo_organization: id, + ...(organization?.org_type === "govt" && { organization: id }), page: qParams.page, limit: resultsPerPage, offset: (qParams.page - 1) * resultsPerPage, ...advancedFilter.filter, }, }), - enabled: !!id, + enabled: !!id && !!organization, }); if (!id) { @@ -47,7 +50,11 @@ export default function OrganizationPatients({ id, navOrganizationId }: Props) { } return ( - +

Patients

diff --git a/src/pages/Organization/components/OrganizationLayout.tsx b/src/pages/Organization/components/OrganizationLayout.tsx index ab80cd485a1..1668a8c80f1 100644 --- a/src/pages/Organization/components/OrganizationLayout.tsx +++ b/src/pages/Organization/components/OrganizationLayout.tsx @@ -1,5 +1,6 @@ import { useQuery } from "@tanstack/react-query"; import { Link, usePath } from "raviger"; +import { useEffect } from "react"; import CareIcon, { IconName } from "@/CAREUI/icons/CareIcon"; @@ -15,6 +16,7 @@ import { Menubar, MenubarMenu, MenubarTrigger } from "@/components/ui/menubar"; import Page from "@/components/Common/Page"; import query from "@/Utils/request/query"; +import { usePermissions } from "@/context/PermissionContext"; import { Organization, OrganizationParent, @@ -26,63 +28,78 @@ interface Props { navOrganizationId?: string; id: string; children: React.ReactNode; + setOrganization?: (org: Organization) => void; } interface NavItem { path: string; title: string; icon: IconName; + visibility: boolean; } export default function OrganizationLayout({ id, navOrganizationId, children, + setOrganization, }: Props) { const path = usePath() || ""; + const { hasPermission } = usePermissions(); const baseUrl = navOrganizationId ? `/organization/${navOrganizationId}/children` : `/organization`; + + const { data: org, isLoading } = useQuery({ + queryKey: ["organization", id], + queryFn: query(organizationApi.get, { + pathParams: { id }, + }), + enabled: !!id, + }); + + useEffect(() => { + if (org) { + setOrganization?.(org); + } + }, [org, setOrganization]); + + if (isLoading) { + return
Loading...
; + } + // add loading state + if (!org) { + return
Not found
; + } + const navItems: NavItem[] = [ { path: `${baseUrl}/${id}`, title: "Organizations", icon: "d-hospital", + visibility: hasPermission("can_view_organization", org.permissions), }, { path: `${baseUrl}/${id}/users`, title: "Users", icon: "d-people", + visibility: hasPermission("can_list_organization_users", org.permissions), }, { path: `${baseUrl}/${id}/patients`, title: "Patients", icon: "d-patient", + visibility: hasPermission("can_list_patients", org.permissions), }, { path: `${baseUrl}/${id}/facilities`, title: "Facilities", icon: "d-hospital", + visibility: hasPermission("can_read_facility", org.permissions), }, ]; - const { data: org, isLoading } = useQuery({ - queryKey: ["organization", id], - queryFn: query(organizationApi.get, { - pathParams: { id }, - }), - enabled: !!id, - }); - - if (isLoading) { - return
Loading...
; - } - // add loading state - if (!org) { - return
Not found
; - } - const orgParents: OrganizationParent[] = []; let currentParent = org.parent; while (currentParent) { @@ -121,23 +138,25 @@ export default function OrganizationLayout({ {/* Navigation */}
- {navItems.map((item) => ( - - - - - {item.title} - - - - ))} + {navItems + .filter((item) => item.visibility) + .map((item) => ( + + + + + {item.title} + + + + ))}
{/* Page Content */} diff --git a/src/types/organization/organization.ts b/src/types/organization/organization.ts index 68a11895758..5b36828ed09 100644 --- a/src/types/organization/organization.ts +++ b/src/types/organization/organization.ts @@ -33,6 +33,7 @@ export interface Organization { created_at: string; updated_at: string; metadata: Metadata | null; + permissions: string[]; } export interface OrganizationUserRole {