diff --git a/lib/mv_web/live/membership_fee_type_live/index.ex b/lib/mv_web/live/membership_fee_type_live/index.ex index 609f91f..37c2746 100644 --- a/lib/mv_web/live/membership_fee_type_live/index.ex +++ b/lib/mv_web/live/membership_fee_type_live/index.ex @@ -18,15 +18,20 @@ defmodule MvWeb.MembershipFeeTypeLive.Index do alias Mv.MembershipFees alias Mv.MembershipFees.MembershipFeeType + alias Mv.Membership alias Mv.Membership.Member alias MvWeb.Helpers.MembershipFeeHelpers @impl true def mount(_params, _session, socket) do + fee_types = load_membership_fee_types() + member_counts = load_member_counts(fee_types) + {:ok, socket |> assign(:page_title, gettext("Membership Fee Types")) - |> assign(:membership_fee_types, load_membership_fee_types())} + |> assign(:membership_fee_types, fee_types) + |> assign(:member_counts, member_counts)} end @impl true @@ -66,7 +71,7 @@ defmodule MvWeb.MembershipFeeTypeLive.Index do <:col :let={mft} label={gettext("Members")}> - {get_member_count(mft)} + {get_member_count(mft, @member_counts)} <:action :let={mft}> @@ -82,18 +87,20 @@ defmodule MvWeb.MembershipFeeTypeLive.Index do data-confirm={gettext("Are you sure?")} class={[ "btn btn-ghost btn-xs", - if(get_member_count(mft) > 0, + if(get_member_count(mft, @member_counts) > 0, do: "text-error opacity-50 cursor-not-allowed", else: "text-error" ) ]} title={ - if get_member_count(mft) > 0, + if get_member_count(mft, @member_counts) > 0, do: - gettext("Cannot delete - %{count} member(s) assigned", count: get_member_count(mft)), + gettext("Cannot delete - %{count} member(s) assigned", + count: get_member_count(mft, @member_counts) + ), else: gettext("Delete") } - disabled={get_member_count(mft) > 0} + disabled={get_member_count(mft, @member_counts) > 0} > <.icon name="hero-trash" class="size-4" /> @@ -112,10 +119,12 @@ defmodule MvWeb.MembershipFeeTypeLive.Index do case Ash.destroy(fee_type, domain: MembershipFees) do :ok -> updated_types = Enum.reject(socket.assigns.membership_fee_types, &(&1.id == id)) + updated_counts = Map.delete(socket.assigns.member_counts, id) {:noreply, socket |> assign(:membership_fee_types, updated_types) + |> assign(:member_counts, updated_counts) |> put_flash(:info, gettext("Membership fee type deleted"))} {:error, error} -> @@ -131,12 +140,27 @@ defmodule MvWeb.MembershipFeeTypeLive.Index do |> Ash.read!(domain: MembershipFees) end - defp get_member_count(fee_type) do - # Count members with this fee type - case Ash.count(Member |> Ash.Query.filter(membership_fee_type_id == ^fee_type.id)) do - {:ok, count} -> count - _ -> 0 - end + # Loads all member counts for fee types in a single query to avoid N+1 queries + defp load_member_counts(fee_types) do + fee_type_ids = Enum.map(fee_types, & &1.id) + + # Load all members with membership_fee_type_id in a single query + members = + Member + |> Ash.Query.filter(membership_fee_type_id in ^fee_type_ids) + |> Ash.Query.select([:membership_fee_type_id]) + |> Ash.read!(domain: Membership) + + # Group by membership_fee_type_id and count + members + |> Enum.group_by(& &1.membership_fee_type_id) + |> Enum.map(fn {fee_type_id, members_list} -> {fee_type_id, length(members_list)} end) + |> Map.new() + end + + # Gets member count from preloaded assigns map + defp get_member_count(fee_type, member_counts) do + Map.get(member_counts, fee_type.id, 0) end defp format_error(%Ash.Error.Invalid{} = error) do