defmodule MvWeb.JoinRequestLive.Index do @moduledoc """ LiveView for listing and reviewing join requests (approval UI, Step 2). ## Features - List join requests filtered by status (default: submitted) - Navigate to detail view for approve/reject actions - Accessible to normal_user and admin roles only ## Security - Page access controlled by CheckPagePermission plug and can_access_page? guard - Ash policy (HasPermission) enforces JoinRequest read :all for normal_user and admin """ use MvWeb, :live_view require Logger import MvWeb.LiveHelpers, only: [current_actor: 1] import MvWeb.Authorization alias Mv.Membership alias MvWeb.Helpers.DateFormatter alias MvWeb.JoinRequestLive.Helpers, as: JoinRequestHelpers @impl true def mount(_params, _session, socket) do actor = current_actor(socket) cond do not Membership.join_form_enabled?() -> {:ok, redirect(socket, to: ~p"/members")} not can_access_page?(actor, "/join_requests") -> {:ok, redirect(socket, to: ~p"/members")} true -> {:ok, load_join_requests(socket, actor)} end end @impl true def render(assigns) do ~H""" <.header> {gettext("Join requests")}

{gettext("Open requests")}

<%= if Enum.empty?(@join_requests) do %>

{gettext("No submitted join requests")}

<% else %> <.table id="join-requests-table" rows={@join_requests} row_id={fn req -> "join-request-#{req.id}" end} row_click={fn req -> JS.navigate(~p"/join_requests/#{req.id}") end} row_tooltip={gettext("Click for details")} > <:col :let={req} label={gettext("Submitted at")}> <%= if req.submitted_at do %> {DateFormatter.format_datetime(req.submitted_at)} <% else %> <.empty_cell sr_text={gettext("Not submitted yet")} /> <% end %> <:col :let={req} label={gettext("First name")}> <.maybe_value value={req.first_name} empty_sr_text={gettext("Not specified")}> {req.first_name} <:col :let={req} label={gettext("Last name")}> <.maybe_value value={req.last_name} empty_sr_text={gettext("Not specified")}> {req.last_name} <:col :let={req} label={gettext("Email")}> {req.email} <:col :let={req} label={gettext("Status")}> <.badge variant={JoinRequestHelpers.status_badge_variant(req.status)}> {JoinRequestHelpers.format_status(req.status)} <% end %>

{gettext("History")}

<%= if Enum.empty?(@join_requests_history) do %>

{gettext("No approved or rejected requests yet")}

<% else %> <.table id="join-requests-history-table" rows={@join_requests_history} row_id={fn req -> "join-request-history-#{req.id}" end} row_click={fn req -> JS.navigate(~p"/join_requests/#{req.id}") end} row_tooltip={gettext("Click for details")} > <:col :let={req} label={gettext("Email")}> {req.email} <:col :let={req} label={gettext("First name")}> <.maybe_value value={req.first_name} empty_sr_text={gettext("Not specified")}> {req.first_name} <:col :let={req} label={gettext("Last name")}> <.maybe_value value={req.last_name} empty_sr_text={gettext("Not specified")}> {req.last_name} <:col :let={req} label={gettext("Status")}> <.badge variant={JoinRequestHelpers.status_badge_variant(req.status)}> {JoinRequestHelpers.format_status(req.status)} <:col :let={req} label={gettext("Reviewed at")}> {review_date(req)} <:col :let={req} label={gettext("Review by")}> {JoinRequestHelpers.reviewer_display(req) || ""} <% end %>
""" end defp load_join_requests(socket, actor) do socket = case Membership.list_join_requests(actor: actor, status: :submitted) do {:ok, requests} -> assign(socket, :join_requests, requests) {:error, error} -> Logger.warning("Failed to load join requests: #{inspect(error)}") assign(socket, :join_requests, []) end socket = case Membership.list_join_requests_history(actor: actor) do {:ok, history} -> assign(socket, :join_requests_history, history) {:error, error} -> Logger.warning("Failed to load join requests history: #{inspect(error)}") assign(socket, :join_requests_history, []) end assign(socket, :page_title, gettext("Join requests")) end defp review_date(req) do date = case req.status do :approved -> req.approved_at :rejected -> req.rejected_at _ -> nil end if date, do: DateFormatter.format_datetime(date), else: "" end end