diff --git a/apps/edr-freight-web/backoffice/src/locales/am/translation.json b/apps/edr-freight-web/backoffice/src/locales/am/translation.json index 1353a9b33..09acb8c62 100644 --- a/apps/edr-freight-web/backoffice/src/locales/am/translation.json +++ b/apps/edr-freight-web/backoffice/src/locales/am/translation.json @@ -7492,12 +7492,22 @@ "loadError": "አስተዳዳሪዎችን መጫን አልተሳካም።", "pickerError": "ድርጅቶችን መጫን አልተሳካም።", "addAdmin": "አስተዳዳሪ ጨምር", + "managePermissions": "ፍቃዶችን ያስተዳድሩ", "add": { "title": "አስተዳዳሪ ጨምር", "description": "የተጠቃሚ መለያ ይፍጠሩ እና በዚህ ድርጅት ውስጥ የአስተዳዳሪ መዳረሻ ይስጡ።", "submit": "አስተዳዳሪ ጨምር", "inviteNote": "ተጠቃሚው ይፈጠራል እና የይለፍ ቃሉን እንዲያዘጋጅ የኤስኤምኤስ ግብዣ ይደርሰዋል።", "noUnitsOrgAdmin": "ይህ ድርጅት ክፍሎች የሉትም — አስተዳዳሪው እንደ የድርጅት አስተዳዳሪ ይጨመራል።" + }, + "permissions": { + "title": "የድርጅት አስተዳዳሪ ፍቃዶች", + "subtitle": "እያንዳንዱ የድርጅት አስተዳዳሪ በመድረኩ ላይ ምን ማድረግ እንደሚችል ይምረጡ።", + "backToAdmins": "ወደ ድርጅት አስተዳዳሪዎች ይመለሱ", + "roleNotFound": "የድርጅት አስተዳዳሪ ሚና ማግኘት አልተቻለም።", + "saved": "የድርጅት አስተዳዳሪ ፍቃዶች ተዘምነዋል።", + "saveFailed": "የድርጅት አስተዳዳሪ ፍቃዶችን ማዘመን አልተቻለም።", + "cannotClearAll": "ተቀምጧል። ፍቃዶቹ አልተቀየሩም — የድርጅት አስተዳዳሪ ሚና ቢያንስ አንድ ፍቃድ ሊኖረው ይገባል።" } } } diff --git a/apps/edr-freight-web/backoffice/src/locales/en/translation.json b/apps/edr-freight-web/backoffice/src/locales/en/translation.json index 1a2a95244..3fa0f0ee3 100644 --- a/apps/edr-freight-web/backoffice/src/locales/en/translation.json +++ b/apps/edr-freight-web/backoffice/src/locales/en/translation.json @@ -7493,12 +7493,22 @@ "loadError": "Failed to load admins.", "pickerError": "Failed to load organizations.", "addAdmin": "Add Admin", + "managePermissions": "Manage Permissions", "add": { "title": "Add Admin", "description": "Create a user account and grant admin access in this organization.", "submit": "Add Admin", "inviteNote": "The user is created and receives an SMS invitation to set their password.", "noUnitsOrgAdmin": "This organization has no units — the admin will be added as an organization admin." + }, + "permissions": { + "title": "Organization Admin Permissions", + "subtitle": "Choose what every Organization Admin can do across the platform.", + "backToAdmins": "Back to Organization Admins", + "roleNotFound": "Could not find the Organization Admin role.", + "saved": "Organization Admin permissions updated.", + "saveFailed": "Failed to update Organization Admin permissions.", + "cannotClearAll": "Saved. Permissions were left unchanged — the Organization Admin role must keep at least one permission." } } } diff --git a/apps/edr-freight-web/backoffice/src/super-admin/components/org-admins/OrgAdminPermissionsPage.tsx b/apps/edr-freight-web/backoffice/src/super-admin/components/org-admins/OrgAdminPermissionsPage.tsx new file mode 100644 index 000000000..a98d38143 --- /dev/null +++ b/apps/edr-freight-web/backoffice/src/super-admin/components/org-admins/OrgAdminPermissionsPage.tsx @@ -0,0 +1,199 @@ +import { useEffect, useMemo, useRef, useState } from "react"; +import { useTranslation } from "react-i18next"; +import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; +import { toast } from "sonner"; +import { Link } from "react-router-dom"; +import { ArrowLeft } from "lucide-react"; +import { Button } from "@/shared/common/ui/button"; +import { Card, CardContent, CardHeader, CardTitle } from "@/shared/common/ui/card"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/shared/common/ui/select"; +import { useLocalizedName } from "@/shared/common/localizedName"; +import { useApplications } from "@/user-management/hooks/useApplications"; +import { PermissionSearch } from "@/user-management/components/position-management/PermissionSearch"; +import { getRoles } from "@/super-admin/services/api/roleService"; +import { + assignPermissionsToRole, + getPermissionsByRoleId, +} from "@/super-admin/services/api/rolePermissionService"; +import { ORG_ADMIN_ROLE_KEY } from "./OrgAdminsColumnDefn"; + +// The Organization Admin role is a fixed, singleton role (unlike position +// types, which are org/unit-scoped) — so this page has no picker, just the +// one role's permission set. +export default function OrgAdminPermissionsPage() { + const { t } = useTranslation(); + const localizedName = useLocalizedName(); + const queryClient = useQueryClient(); + + const [selectedApplicationId, setSelectedApplicationId] = useState(""); + const [permissions, setPermissions] = useState([]); + const hasLoadedPermissions = useRef(false); + // Permissions the role had when the page opened. Needed because the API + // cannot represent "no permissions" (see the save handler). + const loadedPermissionCount = useRef(0); + + const { applications, isLoading: isLoadingApplications } = useApplications(); + + const { + data: rolesResponse, + isLoading: isLoadingRoles, + isError: isRolesError, + } = useQuery({ queryKey: ["roles"], queryFn: getRoles }); + + const orgAdminRole = useMemo( + () => rolesResponse?.data?.items?.find((r) => r.key === ORG_ADMIN_ROLE_KEY), + [rolesResponse], + ); + + const { + data: rolePermissionsResponse, + isSuccess: isPermissionsSuccess, + isError: isPermissionsError, + isLoading: isLoadingPermissions, + } = useQuery({ + queryKey: ["role-permissions", orgAdminRole?.id], + queryFn: () => getPermissionsByRoleId(orgAdminRole!.id), + enabled: !!orgAdminRole?.id, + }); + + useEffect(() => { + if (hasLoadedPermissions.current) return; + if (!isPermissionsSuccess && !isPermissionsError) return; + const ids = rolePermissionsResponse?.data?.items?.map((p) => p.id) ?? []; + loadedPermissionCount.current = ids.length; + setPermissions(ids); + hasLoadedPermissions.current = true; + }, [isPermissionsSuccess, isPermissionsError, rolePermissionsResponse]); + + const handlePermissionChange = (permissionId: string, checked: boolean) => { + setPermissions((prev) => + checked ? [...prev, permissionId] : prev.filter((id) => id !== permissionId), + ); + }; + + const mustClearAll = + permissions.length === 0 && loadedPermissionCount.current > 0; + + const { mutate: save, isPending: isSaving } = useMutation({ + mutationFn: async () => { + if (!orgAdminRole?.id || permissions.length === 0) return; + await assignPermissionsToRole({ + firstId: orgAdminRole.id, + secondIds: permissions, + }); + }, + onSuccess: () => { + loadedPermissionCount.current = permissions.length; + queryClient.invalidateQueries({ queryKey: ["role-permissions"] }); + toast[mustClearAll ? "warning" : "success"]( + t( + mustClearAll + ? "orgAdmins.permissions.cannotClearAll" + : "orgAdmins.permissions.saved", + ), + ); + }, + onError: () => { + toast.error(t("orgAdmins.permissions.saveFailed")); + }, + }); + + const selectedPermissionCount = permissions.length; + + return ( +
+ + + + + {t("orgAdmins.permissions.backToAdmins")} + + + {t("orgAdmins.permissions.title")} + +

+ {t("orgAdmins.permissions.subtitle")} +

+
+ + {isLoadingRoles ? ( +
+ {t("common.loading")} +
+ ) : isRolesError || !orgAdminRole ? ( +
+ {t("orgAdmins.permissions.roleNotFound")} +
+ ) : ( + <> +
+ + +
+ +
+ + +
+ +
+ +
+ + )} +
+
+
+ ); +} diff --git a/apps/edr-freight-web/backoffice/src/super-admin/components/org-admins/OrgAdminsPage.tsx b/apps/edr-freight-web/backoffice/src/super-admin/components/org-admins/OrgAdminsPage.tsx index d5844a603..53ac19841 100644 --- a/apps/edr-freight-web/backoffice/src/super-admin/components/org-admins/OrgAdminsPage.tsx +++ b/apps/edr-freight-web/backoffice/src/super-admin/components/org-admins/OrgAdminsPage.tsx @@ -1,7 +1,15 @@ import { useEffect, useMemo, useState } from "react"; import { useTranslation } from "react-i18next"; import { toast } from "sonner"; -import { Building2, Loader2, Plus, UserPlus, Users2 } from "lucide-react"; +import { Link } from "react-router-dom"; +import { + Building2, + Loader2, + Plus, + ShieldCheck, + UserPlus, + Users2, +} from "lucide-react"; import { Button } from "@/shared/common/ui/button"; import { Card, @@ -190,12 +198,22 @@ export default function OrgAdminsPage() {
- - {t("orgAdmins.title")} - -

- {t("orgAdmins.subtitle")} -

+
+
+ + {t("orgAdmins.title")} + +

+ {t("orgAdmins.subtitle")} +

+
+ +
{/* Org selector + summary */} diff --git a/apps/edr-freight-web/backoffice/src/super-admin/services/api/rolePermissionService.ts b/apps/edr-freight-web/backoffice/src/super-admin/services/api/rolePermissionService.ts new file mode 100644 index 000000000..1d96edc1f --- /dev/null +++ b/apps/edr-freight-web/backoffice/src/super-admin/services/api/rolePermissionService.ts @@ -0,0 +1,25 @@ +import { withHeaders } from "@/record-management/services/api/withHeaders"; +import axiosInstance from "@/shared/services/axiosInstance"; +import { PermissionListResponse } from "@/user-management/dto/permissions/permissonDto"; +import { AxiosResponse } from "axios"; + +export interface AssignRolePermissionsPayload { + firstId: string; + secondIds: string[]; +} + +// GET /role-permissions/given-first/{roleId} +export const getPermissionsByRoleId = async ( + roleId: string, +): Promise> => + axiosInstance.get(`/role-permissions/given-first/${roleId}`, { + headers: withHeaders(), + }); + +// POST /role-permissions/assign-seconds-for-first +export const assignPermissionsToRole = async ( + payload: AssignRolePermissionsPayload, +): Promise> => + axiosInstance.post("/role-permissions/assign-seconds-for-first", payload, { + headers: withHeaders(), + }); diff --git a/apps/edr-freight-web/backoffice/src/super-admin/services/api/roleService.ts b/apps/edr-freight-web/backoffice/src/super-admin/services/api/roleService.ts new file mode 100644 index 000000000..6ede479d8 --- /dev/null +++ b/apps/edr-freight-web/backoffice/src/super-admin/services/api/roleService.ts @@ -0,0 +1,20 @@ +import { withHeaders } from "@/record-management/services/api/withHeaders"; +import axiosInstance from "@/shared/services/axiosInstance"; +import { AxiosResponse } from "axios"; + +export interface RoleDto { + id: string; + name: { am: string; en: string }; + key: string; +} + +export interface RoleListResponse { + count: number; + items: RoleDto[]; +} + +export const getRoles = async (): Promise> => + axiosInstance.get("/roles", { + headers: withHeaders(), + params: { take: 100 }, + }); diff --git a/apps/edr-freight-web/backoffice/src/user-management/route.tsx b/apps/edr-freight-web/backoffice/src/user-management/route.tsx index 76d3182ad..af29cdc5e 100644 --- a/apps/edr-freight-web/backoffice/src/user-management/route.tsx +++ b/apps/edr-freight-web/backoffice/src/user-management/route.tsx @@ -19,6 +19,7 @@ import { AppLayout } from "./Applayout"; import ActivityLogPage from "@/pages/ActivityLogPage"; import AdminRegistrationPage from "@/pages/Organizations/AdminRegistrationPage"; import OrganizationAdminsPage from "@/pages/OrganizationAdminsPage"; +import OrgAdminPermissionsPage from "@/super-admin/components/org-admins/OrgAdminPermissionsPage"; import UserProfileEditPage from "@/pages/UserProfileEditPage"; import UploadedDocumentViewPage from "@/pages/UploadedDocumentViewPage"; import EditOrganizationPage from "@/pages/Organizations/EditOrganizationPage"; @@ -197,6 +198,10 @@ export function UserManagementRoutes(): ReactElement { path="user-management/organization_admins" element={} /> + } + /> }