defmodule MvWeb.RoleLive.Show do @moduledoc """ LiveView for displaying a single role's details. ## Features - Display role information (name, description, permission_set_name, is_system_role) - Navigate to edit form - Return to role list ## Security Only admins can access this page (enforced by authorization). """ use MvWeb, :live_view alias Mv.Accounts require Ash.Query on_mount {MvWeb.LiveHelpers, :ensure_user_role_loaded} @impl true def mount(%{"id" => id}, _session, socket) do try do case Ash.get( Mv.Authorization.Role, id, domain: Mv.Authorization, actor: socket.assigns[:current_user] ) do {:ok, role} -> user_count = load_user_count(role) {:ok, socket |> assign(:page_title, gettext("Show Role")) |> assign(:role, role) |> assign(:user_count, user_count)} {:error, %Ash.Error.Invalid{errors: [%Ash.Error.Query.NotFound{} | _]}} -> {:ok, socket |> put_flash(:error, gettext("Role not found.")) |> redirect(to: ~p"/admin/roles")} {:error, error} -> {:ok, socket |> put_flash(:error, format_error(error)) |> redirect(to: ~p"/admin/roles")} end rescue e in [Ash.Error.Invalid] -> case e do %Ash.Error.Invalid{errors: [%Ash.Error.Query.NotFound{} | _]} -> {:ok, socket |> put_flash(:error, gettext("Role not found.")) |> redirect(to: ~p"/admin/roles")} _ -> reraise e, __STACKTRACE__ end end end @impl true def handle_event("delete", %{"id" => id}, socket) do case Mv.Authorization.get_role(id, actor: socket.assigns.current_user) do {:ok, role} -> handle_delete_role(role, socket) {:error, %Ash.Error.Query.NotFound{}} -> {:noreply, put_flash( socket, :error, gettext("Role not found.") ) |> push_navigate(to: ~p"/admin/roles")} {:error, error} -> error_message = format_error(error) {:noreply, put_flash( socket, :error, gettext("Failed to delete role: %{error}", error: error_message) )} end end defp handle_delete_role(role, socket) do if role.is_system_role do {:noreply, put_flash( socket, :error, gettext("System roles cannot be deleted.") )} else user_count = recalculate_user_count(role) if user_count > 0 do {:noreply, put_flash( socket, :error, gettext( "Cannot delete role. %{count} user(s) are still assigned to this role. Please assign them to another role first.", count: user_count ) )} else perform_role_deletion(role, socket) end end end defp perform_role_deletion(role, socket) do case Mv.Authorization.destroy_role(role, actor: socket.assigns.current_user) do :ok -> {:noreply, socket |> put_flash(:info, gettext("Role deleted successfully.")) |> push_navigate(to: ~p"/admin/roles")} {:error, error} -> error_message = format_error(error) {:noreply, put_flash( socket, :error, gettext("Failed to delete role: %{error}", error: error_message) )} end end defp recalculate_user_count(role) do case Ash.count(Accounts.User |> Ash.Query.filter(role_id == ^role.id)) do {:ok, count} -> count _ -> 0 end end defp load_user_count(role) do recalculate_user_count(role) end @impl true def render(assigns) do ~H""" <.header> {gettext("Role")} {@role.name} <:subtitle>{gettext("Role details and permissions.")} <:actions> <.button navigate={~p"/admin/roles"} aria-label={gettext("Back to roles list")}> <.icon name="hero-arrow-left" /> {gettext("Back to roles list")} <%= if can?(@current_user, :update, Mv.Authorization.Role) do %> <.button variant="primary" navigate={~p"/admin/roles/#{@role}/edit"}> <.icon name="hero-pencil-square" /> {gettext("Edit Role")} <% end %> <%= if can?(@current_user, :destroy, Mv.Authorization.Role) and not @role.is_system_role do %> <.link phx-click={JS.push("delete", value: %{id: @role.id})} data-confirm={gettext("Are you sure?")} class="btn btn-error" > <.icon name="hero-trash" /> {gettext("Delete Role")} <% end %> <.list> <:item title={gettext("Name")}>{@role.name} <:item title={gettext("Description")}> <%= if @role.description do %> {@role.description} <% else %> {gettext("No description")} <% end %> <:item title={gettext("Permission Set")}> {@role.permission_set_name} <:item title={gettext("System Role")}> <%= if @role.is_system_role do %> {gettext("Yes")} <% else %> {gettext("No")} <% end %> """ end defp format_error(%Ash.Error.Invalid{} = error) do Enum.map_join(error.errors, ", ", fn e -> e.message end) end defp format_error(error) when is_binary(error), do: error defp format_error(_error), do: gettext("An error occurred") defp permission_set_badge_class("own_data"), do: "badge badge-neutral badge-sm" defp permission_set_badge_class("read_only"), do: "badge badge-info badge-sm" defp permission_set_badge_class("normal_user"), do: "badge badge-success badge-sm" defp permission_set_badge_class("admin"), do: "badge badge-error badge-sm" defp permission_set_badge_class(_), do: "badge badge-ghost badge-sm" end