defmodule MvWeb.MemberLive.Show.DeactivateComponent do @moduledoc """ LiveComponent owning the member deactivate/reactivate sub-flow on the member show page. ## Features - Deactivate control (shown when the member has no exit_date) - Reactivate control (shown when the member has an exit_date) - Date-selection modal (default: today, future dates allowed) for deactivation - Routes both actions through `Membership.update_member/2` with the real user actor, inheriting the `exit_date > join_date` validation and the cycle-regeneration hook. Controls are gated on `:update` permission for the member. On success the component notifies the parent with `{:member_updated, member}`, which the parent already handles. """ use MvWeb, :live_component import MvWeb.Authorization, only: [can?: 3] alias Mv.Membership alias MvWeb.Helpers.MemberHelpers @impl true def render(assigns) do ~H"""
<%= if @can_update do %>

{gettext("Membership status")}

<%= if @member.exit_date do %>

{gettext( "This member is deactivated (exit date set). Reactivating clears the exit date." )}

<.button id="reactivate-member-trigger" data-testid="member-reactivate" variant="primary" phx-click="reactivate" phx-target={@myself} aria-label={ gettext("Reactivate member %{name}", name: MemberHelpers.display_name(@member)) } > <.icon name="hero-arrow-uturn-left" class="size-4" /> {gettext("Reactivate member")} <% else %>

{gettext( "Deactivating this member records an exit date. You can reactivate them later." )}

<.button id="deactivate-member-trigger" data-testid="member-deactivate" variant="outline" phx-click="open_modal" phx-target={@myself} aria-label={ gettext("Deactivate member %{name}", name: MemberHelpers.display_name(@member)) } > <.icon name="hero-arrow-right-on-rectangle" class="size-4" /> {gettext("Deactivate member")} <% end %>
<% end %> <%= if @show_modal do %> <% end %>
""" end @impl true def update(assigns, socket) do {:ok, socket |> assign(assigns) |> assign(:can_update, can?(assigns.current_user, :update, assigns.member)) |> assign_new(:show_modal, fn -> false end) |> assign_new(:exit_date, fn -> Date.utc_today() end) |> assign_new(:error, fn -> nil end)} end @impl true def handle_event("open_modal", _params, socket) do {:noreply, socket |> assign(:show_modal, true) |> assign(:exit_date, Date.utc_today()) |> assign(:error, nil)} end def handle_event("cancel_modal", _params, socket) do {:noreply, close_modal(socket)} end def handle_event("dialog_keydown", %{"key" => key}, socket) when key in ["Escape", "Esc"] do {:noreply, close_modal(socket)} end def handle_event("dialog_keydown", _params, socket), do: {:noreply, socket} def handle_event("deactivate", %{"exit_date" => exit_date_str}, socket) do case Date.from_iso8601(exit_date_str) do {:ok, exit_date} -> apply_exit_date(socket, exit_date, show_inline_error: true) {:error, _reason} -> {:noreply, assign(socket, :error, gettext("Invalid date format"))} end end def handle_event("reactivate", _params, socket) do apply_exit_date(socket, nil, show_inline_error: false) end defp apply_exit_date(socket, exit_date, opts) do member = socket.assigns.member actor = socket.assigns.current_user case Membership.update_member(member, %{exit_date: exit_date}, actor: actor) do {:ok, updated_member} -> send(self(), {:member_updated, updated_member}) {:noreply, socket |> assign(:member, updated_member) |> close_modal()} {:error, error} -> if opts[:show_inline_error] do {:noreply, assign(socket, :error, format_error(error))} else send(self(), {:put_flash, :error, format_error(error)}) {:noreply, socket} end end end defp close_modal(socket) do socket |> assign(:show_modal, false) |> assign(:error, nil) end defp format_error(%Ash.Error.Invalid{errors: errors}) do Enum.map_join(errors, ", ", fn %{message: message} -> message other -> inspect(other) end) end defp format_error(%Ash.Error.Forbidden{}) do gettext("You are not allowed to perform this action.") end defp format_error(_error), do: gettext("An error occurred") end