mitgliederverwaltung/lib/mv_web/member_live/index/membership_fee_status.ex
Moritz c65b3808bf
Refactor filters to use cycle status instead of paid field
Replace paid_filter with cycle_status_filter that filters based on
membership fee cycle status (last or current cycle). Update
PaymentFilterComponent to use new filter with options All, Paid, Unpaid.
Remove membership fee status filter dropdown. Extend
filter_members_by_cycle_status/3 to support both paid and unpaid filtering.
Update toggle_cycle_view to preserve filter state in URL.
2025-12-18 15:11:02 +01:00

179 lines
5.7 KiB
Elixir

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 cycle status (paid or unpaid).
Returns members that have the specified status in either the last completed cycle
or the current cycle, depending on `show_current`.
## Parameters
- `members` - List of member structs with loaded cycles
- `status` - Cycle status to filter by (`:paid` or `:unpaid`)
- `show_current` - If true, filter by current cycle; if false, filter by last completed cycle
## Returns
List of members with the specified cycle status
## Examples
# Filter unpaid members in last cycle
iex> filter_members_by_cycle_status(members, :unpaid, false)
[%Member{}, ...]
# Filter paid members in current cycle
iex> filter_members_by_cycle_status(members, :paid, true)
[%Member{}, ...]
"""
@spec filter_members_by_cycle_status([Member.t()], :paid | :unpaid, boolean()) :: [Member.t()]
def filter_members_by_cycle_status(members, status, show_current \\ false)
when status in [:paid, :unpaid] do
Enum.filter(members, fn member ->
member_status = get_cycle_status_for_member(member, show_current)
member_status == status
end)
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
## Deprecated
This function is kept for backwards compatibility. Use `filter_members_by_cycle_status/3` instead.
"""
@spec filter_unpaid_members([Member.t()], boolean()) :: [Member.t()]
def filter_unpaid_members(members, show_current \\ false) do
filter_members_by_cycle_status(members, :unpaid, show_current)
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