defmodule MvWeb.MemberLive.Index.MembershipFeeStatus do @moduledoc """ Helper module for membership fee status display in member list view. Provides functions to efficiently load and determine cycle status for members in the list view, avoiding N+1 queries. """ use Gettext, backend: MvWeb.Gettext alias Mv.Membership.Member alias MvWeb.Helpers.MembershipFeeHelpers @doc """ Loads membership fee cycles for members efficiently. Preloads cycles with membership_fee_type relationship to avoid N+1 queries. Only loads the relevant cycle per member (last completed or current, depending on show_current). ## Parameters - `query` - Ash query for members - `show_current` - If true, load current cycle; if false, load last completed cycle - `today` - Optional date to use as reference (defaults to today) ## Returns Modified query with cycles loaded ## Performance Uses Ash.Query.load to efficiently preload cycles in a single query. Filters cycles at database level to only load the relevant cycle per member. """ @spec load_cycles_for_members(Ash.Query.t(), boolean(), Date.t() | nil) :: Ash.Query.t() def load_cycles_for_members(query, _show_current \\ false, _today \\ nil) do # Load membership_fee_type and cycles with efficient filtering query |> Ash.Query.load([:membership_fee_type, membership_fee_cycles: [:membership_fee_type]]) end @doc """ Gets the cycle status for a member. Returns the status of either the last completed cycle or the current cycle, depending on the `show_current` parameter. ## Parameters - `member` - Member struct with loaded cycles and membership_fee_type - `show_current` - If true, get current cycle status; if false, get last completed cycle status ## Returns - `:paid`, `:unpaid`, or `:suspended` if cycle exists - `nil` if no cycle exists ## Examples # Get last completed cycle status iex> MvWeb.MemberLive.Index.MembershipFeeStatus.get_cycle_status_for_member(member, false) :paid # Get current cycle status iex> MvWeb.MemberLive.Index.MembershipFeeStatus.get_cycle_status_for_member(member, true) :unpaid """ @spec get_cycle_status_for_member(Member.t(), boolean()) :: :paid | :unpaid | :suspended | nil def get_cycle_status_for_member(member, show_current \\ false) do cycle = if show_current do MembershipFeeHelpers.get_current_cycle(member) else MembershipFeeHelpers.get_last_completed_cycle(member) end case cycle do nil -> nil cycle -> cycle.status end end @doc """ Formats cycle status as a badge component. Returns a map with badge information for rendering in templates. ## Parameters - `status` - Cycle status (`:paid`, `:unpaid`, `:suspended`, or `nil`) ## Returns Map with `:color`, `:icon`, and `:label` keys, or `nil` if status is nil ## Examples iex> MvWeb.MemberLive.Index.MembershipFeeStatus.format_cycle_status_badge(:paid) %{color: "badge-success", icon: "hero-check-circle", label: "Paid"} iex> MvWeb.MemberLive.Index.MembershipFeeStatus.format_cycle_status_badge(nil) nil """ @spec format_cycle_status_badge(:paid | :unpaid | :suspended | nil) :: %{color: String.t(), icon: String.t(), label: String.t()} | nil def format_cycle_status_badge(nil), do: nil def format_cycle_status_badge(status) when status in [:paid, :unpaid, :suspended] do %{ color: MembershipFeeHelpers.status_color(status), icon: MembershipFeeHelpers.status_icon(status), label: format_status_label(status) } end @doc """ Filters members by unpaid cycle status. Returns members that have unpaid cycles in either the last completed cycle or the current cycle, depending on `show_current`. ## Parameters - `members` - List of member structs with loaded cycles - `show_current` - If true, filter by current cycle; if false, filter by last completed cycle ## Returns List of members with unpaid cycles """ @spec filter_unpaid_members([Member.t()], boolean()) :: [Member.t()] def filter_unpaid_members(members, show_current \\ false) do Enum.filter(members, fn member -> status = get_cycle_status_for_member(member, show_current) status == :unpaid end) end # Private helper function to format status label defp format_status_label(:paid), do: gettext("Paid") defp format_status_label(:unpaid), do: gettext("Unpaid") defp format_status_label(:suspended), do: gettext("Suspended") end