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.
This commit is contained in:
parent
098b3b0a2a
commit
c65b3808bf
10 changed files with 490 additions and 247 deletions
|
|
@ -3,7 +3,7 @@ defmodule MvWeb.Components.PaymentFilterComponentTest do
|
|||
Unit tests for the PaymentFilterComponent.
|
||||
|
||||
Tests cover:
|
||||
- Rendering in all 3 filter states (nil, :paid, :not_paid)
|
||||
- Rendering in all 3 filter states (nil, :paid, :unpaid)
|
||||
- Event emission when selecting options
|
||||
- ARIA attributes for accessibility
|
||||
- Dropdown open/close behavior
|
||||
|
|
@ -25,15 +25,15 @@ defmodule MvWeb.Components.PaymentFilterComponentTest do
|
|||
|
||||
test "renders with paid filter active", %{conn: conn} do
|
||||
conn = conn_with_oidc_user(conn)
|
||||
{:ok, view, _html} = live(conn, "/members?paid_filter=paid")
|
||||
{:ok, view, _html} = live(conn, "/members?cycle_status_filter=paid")
|
||||
|
||||
# Should show badge when filter is active
|
||||
assert has_element?(view, "#payment-filter .badge")
|
||||
end
|
||||
|
||||
test "renders with not_paid filter active", %{conn: conn} do
|
||||
test "renders with unpaid filter active", %{conn: conn} do
|
||||
conn = conn_with_oidc_user(conn)
|
||||
{:ok, view, _html} = live(conn, "/members?paid_filter=not_paid")
|
||||
{:ok, view, _html} = live(conn, "/members?cycle_status_filter=unpaid")
|
||||
|
||||
# Should show badge when filter is active
|
||||
assert has_element?(view, "#payment-filter .badge")
|
||||
|
|
@ -82,7 +82,7 @@ defmodule MvWeb.Components.PaymentFilterComponentTest do
|
|||
describe "filter selection" do
|
||||
test "selecting 'All' clears the filter and updates URL", %{conn: conn} do
|
||||
conn = conn_with_oidc_user(conn)
|
||||
{:ok, view, _html} = live(conn, "/members?paid_filter=paid")
|
||||
{:ok, view, _html} = live(conn, "/members?cycle_status_filter=paid")
|
||||
|
||||
# Open dropdown
|
||||
view
|
||||
|
|
@ -94,7 +94,7 @@ defmodule MvWeb.Components.PaymentFilterComponentTest do
|
|||
|> element("#payment-filter button[phx-value-filter='']")
|
||||
|> render_click()
|
||||
|
||||
# URL should not contain paid_filter param - wait for patch
|
||||
# URL should not contain cycle_status_filter param - wait for patch
|
||||
assert_patch(view)
|
||||
end
|
||||
|
||||
|
|
@ -112,12 +112,12 @@ defmodule MvWeb.Components.PaymentFilterComponentTest do
|
|||
|> element("#payment-filter button[phx-value-filter='paid']")
|
||||
|> render_click()
|
||||
|
||||
# Wait for patch and check URL contains paid_filter=paid
|
||||
# Wait for patch and check URL contains cycle_status_filter=paid
|
||||
path = assert_patch(view)
|
||||
assert path =~ "paid_filter=paid"
|
||||
assert path =~ "cycle_status_filter=paid"
|
||||
end
|
||||
|
||||
test "selecting 'Not paid' sets the filter and updates URL", %{conn: conn} do
|
||||
test "selecting 'Unpaid' sets the filter and updates URL", %{conn: conn} do
|
||||
conn = conn_with_oidc_user(conn)
|
||||
{:ok, view, _html} = live(conn, "/members")
|
||||
|
||||
|
|
@ -126,14 +126,14 @@ defmodule MvWeb.Components.PaymentFilterComponentTest do
|
|||
|> element("#payment-filter button[aria-haspopup='true']")
|
||||
|> render_click()
|
||||
|
||||
# Select "Not paid" option
|
||||
# Select "Unpaid" option
|
||||
view
|
||||
|> element("#payment-filter button[phx-value-filter='not_paid']")
|
||||
|> element("#payment-filter button[phx-value-filter='unpaid']")
|
||||
|> render_click()
|
||||
|
||||
# Wait for patch and check URL contains paid_filter=not_paid
|
||||
# Wait for patch and check URL contains cycle_status_filter=unpaid
|
||||
path = assert_patch(view)
|
||||
assert path =~ "paid_filter=not_paid"
|
||||
assert path =~ "cycle_status_filter=unpaid"
|
||||
end
|
||||
end
|
||||
|
||||
|
|
@ -166,7 +166,7 @@ defmodule MvWeb.Components.PaymentFilterComponentTest do
|
|||
|
||||
test "has aria-checked on selected option", %{conn: conn} do
|
||||
conn = conn_with_oidc_user(conn)
|
||||
{:ok, view, _html} = live(conn, "/members?paid_filter=paid")
|
||||
{:ok, view, _html} = live(conn, "/members?cycle_status_filter=paid")
|
||||
|
||||
# Open dropdown
|
||||
view
|
||||
|
|
|
|||
|
|
@ -235,4 +235,134 @@ defmodule MvWeb.MemberLive.Index.MembershipFeeStatusTest do
|
|||
assert result == nil
|
||||
end
|
||||
end
|
||||
|
||||
describe "filter_members_by_cycle_status/3" do
|
||||
test "filters paid members in last cycle" do
|
||||
fee_type = create_fee_type(%{interval: :yearly})
|
||||
today = Date.utc_today()
|
||||
last_year_start = Date.new!(today.year - 1, 1, 1)
|
||||
|
||||
# Member with paid last cycle
|
||||
member1 = create_member(%{membership_fee_type_id: fee_type.id})
|
||||
create_cycle(member1, fee_type, %{cycle_start: last_year_start, status: :paid})
|
||||
|
||||
# Member with unpaid last cycle
|
||||
member2 = create_member(%{membership_fee_type_id: fee_type.id})
|
||||
create_cycle(member2, fee_type, %{cycle_start: last_year_start, status: :unpaid})
|
||||
|
||||
members =
|
||||
[member1, member2]
|
||||
|> Enum.map(fn m ->
|
||||
m
|
||||
|> Ash.load!(membership_fee_cycles: [:membership_fee_type])
|
||||
|> Ash.load!(:membership_fee_type)
|
||||
end)
|
||||
|
||||
filtered = MembershipFeeStatus.filter_members_by_cycle_status(members, :paid, false)
|
||||
|
||||
assert length(filtered) == 1
|
||||
assert List.first(filtered).id == member1.id
|
||||
end
|
||||
|
||||
test "filters unpaid members in last cycle" do
|
||||
fee_type = create_fee_type(%{interval: :yearly})
|
||||
today = Date.utc_today()
|
||||
last_year_start = Date.new!(today.year - 1, 1, 1)
|
||||
|
||||
# Member with paid last cycle
|
||||
member1 = create_member(%{membership_fee_type_id: fee_type.id})
|
||||
create_cycle(member1, fee_type, %{cycle_start: last_year_start, status: :paid})
|
||||
|
||||
# Member with unpaid last cycle
|
||||
member2 = create_member(%{membership_fee_type_id: fee_type.id})
|
||||
create_cycle(member2, fee_type, %{cycle_start: last_year_start, status: :unpaid})
|
||||
|
||||
members =
|
||||
[member1, member2]
|
||||
|> Enum.map(fn m ->
|
||||
m
|
||||
|> Ash.load!(membership_fee_cycles: [:membership_fee_type])
|
||||
|> Ash.load!(:membership_fee_type)
|
||||
end)
|
||||
|
||||
filtered = MembershipFeeStatus.filter_members_by_cycle_status(members, :unpaid, false)
|
||||
|
||||
assert length(filtered) == 1
|
||||
assert List.first(filtered).id == member2.id
|
||||
end
|
||||
|
||||
test "filters paid members in current cycle" do
|
||||
fee_type = create_fee_type(%{interval: :yearly})
|
||||
today = Date.utc_today()
|
||||
current_year_start = Date.new!(today.year, 1, 1)
|
||||
|
||||
# Member with paid current cycle
|
||||
member1 = create_member(%{membership_fee_type_id: fee_type.id})
|
||||
create_cycle(member1, fee_type, %{cycle_start: current_year_start, status: :paid})
|
||||
|
||||
# Member with unpaid current cycle
|
||||
member2 = create_member(%{membership_fee_type_id: fee_type.id})
|
||||
create_cycle(member2, fee_type, %{cycle_start: current_year_start, status: :unpaid})
|
||||
|
||||
members =
|
||||
[member1, member2]
|
||||
|> Enum.map(fn m ->
|
||||
m
|
||||
|> Ash.load!(membership_fee_cycles: [:membership_fee_type])
|
||||
|> Ash.load!(:membership_fee_type)
|
||||
end)
|
||||
|
||||
filtered = MembershipFeeStatus.filter_members_by_cycle_status(members, :paid, true)
|
||||
|
||||
assert length(filtered) == 1
|
||||
assert List.first(filtered).id == member1.id
|
||||
end
|
||||
|
||||
test "filters unpaid members in current cycle" do
|
||||
fee_type = create_fee_type(%{interval: :yearly})
|
||||
today = Date.utc_today()
|
||||
current_year_start = Date.new!(today.year, 1, 1)
|
||||
|
||||
# Member with paid current cycle
|
||||
member1 = create_member(%{membership_fee_type_id: fee_type.id})
|
||||
create_cycle(member1, fee_type, %{cycle_start: current_year_start, status: :paid})
|
||||
|
||||
# Member with unpaid current cycle
|
||||
member2 = create_member(%{membership_fee_type_id: fee_type.id})
|
||||
create_cycle(member2, fee_type, %{cycle_start: current_year_start, status: :unpaid})
|
||||
|
||||
members =
|
||||
[member1, member2]
|
||||
|> Enum.map(fn m ->
|
||||
m
|
||||
|> Ash.load!(membership_fee_cycles: [:membership_fee_type])
|
||||
|> Ash.load!(:membership_fee_type)
|
||||
end)
|
||||
|
||||
filtered = MembershipFeeStatus.filter_members_by_cycle_status(members, :unpaid, true)
|
||||
|
||||
assert length(filtered) == 1
|
||||
assert List.first(filtered).id == member2.id
|
||||
end
|
||||
|
||||
test "returns all members when filter is nil" do
|
||||
fee_type = create_fee_type(%{interval: :yearly})
|
||||
member1 = create_member(%{membership_fee_type_id: fee_type.id})
|
||||
member2 = create_member(%{membership_fee_type_id: fee_type.id})
|
||||
|
||||
members =
|
||||
[member1, member2]
|
||||
|> Enum.map(fn m ->
|
||||
m
|
||||
|> Ash.load!(membership_fee_cycles: [:membership_fee_type])
|
||||
|> Ash.load!(:membership_fee_type)
|
||||
end)
|
||||
|
||||
# filter_unpaid_members should still work for backwards compatibility
|
||||
filtered = MembershipFeeStatus.filter_unpaid_members(members, false)
|
||||
|
||||
# Both members have no cycles, so both should be filtered out
|
||||
assert length(filtered) == 0
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -456,4 +456,205 @@ defmodule MvWeb.MemberLive.IndexTest do
|
|||
assert has_element?(view, "#flash-group")
|
||||
end
|
||||
end
|
||||
|
||||
describe "cycle status filter" do
|
||||
alias Mv.MembershipFees.MembershipFeeType
|
||||
alias Mv.MembershipFees.MembershipFeeCycle
|
||||
|
||||
# Helper to create a membership fee type
|
||||
defp create_fee_type(attrs) do
|
||||
default_attrs = %{
|
||||
name: "Test Fee Type #{System.unique_integer([:positive])}",
|
||||
amount: Decimal.new("50.00"),
|
||||
interval: :yearly
|
||||
}
|
||||
|
||||
attrs = Map.merge(default_attrs, attrs)
|
||||
|
||||
MembershipFeeType
|
||||
|> Ash.Changeset.for_create(:create, attrs)
|
||||
|> Ash.create!()
|
||||
end
|
||||
|
||||
# Helper to create a member
|
||||
defp create_member(attrs) do
|
||||
default_attrs = %{
|
||||
first_name: "Test",
|
||||
last_name: "Member",
|
||||
email: "test.member.#{System.unique_integer([:positive])}@example.com"
|
||||
}
|
||||
|
||||
attrs = Map.merge(default_attrs, attrs)
|
||||
|
||||
Mv.Membership.Member
|
||||
|> Ash.Changeset.for_create(:create_member, attrs)
|
||||
|> Ash.create!()
|
||||
end
|
||||
|
||||
# Helper to create a cycle
|
||||
defp create_cycle(member, fee_type, attrs) do
|
||||
# Delete any auto-generated cycles first to avoid conflicts
|
||||
existing_cycles =
|
||||
MembershipFeeCycle
|
||||
|> Ash.Query.filter(member_id == ^member.id)
|
||||
|> Ash.read!()
|
||||
|
||||
Enum.each(existing_cycles, fn cycle -> Ash.destroy!(cycle) end)
|
||||
|
||||
default_attrs = %{
|
||||
cycle_start: ~D[2023-01-01],
|
||||
amount: Decimal.new("50.00"),
|
||||
member_id: member.id,
|
||||
membership_fee_type_id: fee_type.id,
|
||||
status: :unpaid
|
||||
}
|
||||
|
||||
attrs = Map.merge(default_attrs, attrs)
|
||||
|
||||
MembershipFeeCycle
|
||||
|> Ash.Changeset.for_create(:create, attrs)
|
||||
|> Ash.create!()
|
||||
end
|
||||
|
||||
test "filter shows only members with paid status in last cycle", %{conn: conn} do
|
||||
conn = conn_with_oidc_user(conn)
|
||||
fee_type = create_fee_type(%{interval: :yearly})
|
||||
today = Date.utc_today()
|
||||
last_year_start = Date.new!(today.year - 1, 1, 1)
|
||||
|
||||
# Member with paid last cycle
|
||||
paid_member =
|
||||
create_member(%{
|
||||
first_name: "PaidLast",
|
||||
membership_fee_type_id: fee_type.id
|
||||
})
|
||||
|
||||
create_cycle(paid_member, fee_type, %{cycle_start: last_year_start, status: :paid})
|
||||
|
||||
# Member with unpaid last cycle
|
||||
unpaid_member =
|
||||
create_member(%{
|
||||
first_name: "UnpaidLast",
|
||||
membership_fee_type_id: fee_type.id
|
||||
})
|
||||
|
||||
create_cycle(unpaid_member, fee_type, %{cycle_start: last_year_start, status: :unpaid})
|
||||
|
||||
{:ok, _view, html} = live(conn, "/members?cycle_status_filter=paid")
|
||||
|
||||
assert html =~ "PaidLast"
|
||||
refute html =~ "UnpaidLast"
|
||||
end
|
||||
|
||||
test "filter shows only members with unpaid status in last cycle", %{conn: conn} do
|
||||
conn = conn_with_oidc_user(conn)
|
||||
fee_type = create_fee_type(%{interval: :yearly})
|
||||
today = Date.utc_today()
|
||||
last_year_start = Date.new!(today.year - 1, 1, 1)
|
||||
|
||||
# Member with paid last cycle
|
||||
paid_member =
|
||||
create_member(%{
|
||||
first_name: "PaidLast",
|
||||
membership_fee_type_id: fee_type.id
|
||||
})
|
||||
|
||||
create_cycle(paid_member, fee_type, %{cycle_start: last_year_start, status: :paid})
|
||||
|
||||
# Member with unpaid last cycle
|
||||
unpaid_member =
|
||||
create_member(%{
|
||||
first_name: "UnpaidLast",
|
||||
membership_fee_type_id: fee_type.id
|
||||
})
|
||||
|
||||
create_cycle(unpaid_member, fee_type, %{cycle_start: last_year_start, status: :unpaid})
|
||||
|
||||
{:ok, _view, html} = live(conn, "/members?cycle_status_filter=unpaid")
|
||||
|
||||
refute html =~ "PaidLast"
|
||||
assert html =~ "UnpaidLast"
|
||||
end
|
||||
|
||||
test "filter shows only members with paid status in current cycle", %{conn: conn} do
|
||||
conn = conn_with_oidc_user(conn)
|
||||
fee_type = create_fee_type(%{interval: :yearly})
|
||||
today = Date.utc_today()
|
||||
current_year_start = Date.new!(today.year, 1, 1)
|
||||
|
||||
# Member with paid current cycle
|
||||
paid_member =
|
||||
create_member(%{
|
||||
first_name: "PaidCurrent",
|
||||
membership_fee_type_id: fee_type.id
|
||||
})
|
||||
|
||||
create_cycle(paid_member, fee_type, %{cycle_start: current_year_start, status: :paid})
|
||||
|
||||
# Member with unpaid current cycle
|
||||
unpaid_member =
|
||||
create_member(%{
|
||||
first_name: "UnpaidCurrent",
|
||||
membership_fee_type_id: fee_type.id
|
||||
})
|
||||
|
||||
create_cycle(unpaid_member, fee_type, %{cycle_start: current_year_start, status: :unpaid})
|
||||
|
||||
{:ok, _view, html} = live(conn, "/members?cycle_status_filter=paid&show_current_cycle=true")
|
||||
|
||||
assert html =~ "PaidCurrent"
|
||||
refute html =~ "UnpaidCurrent"
|
||||
end
|
||||
|
||||
test "filter shows only members with unpaid status in current cycle", %{conn: conn} do
|
||||
conn = conn_with_oidc_user(conn)
|
||||
fee_type = create_fee_type(%{interval: :yearly})
|
||||
today = Date.utc_today()
|
||||
current_year_start = Date.new!(today.year, 1, 1)
|
||||
|
||||
# Member with paid current cycle
|
||||
paid_member =
|
||||
create_member(%{
|
||||
first_name: "PaidCurrent",
|
||||
membership_fee_type_id: fee_type.id
|
||||
})
|
||||
|
||||
create_cycle(paid_member, fee_type, %{cycle_start: current_year_start, status: :paid})
|
||||
|
||||
# Member with unpaid current cycle
|
||||
unpaid_member =
|
||||
create_member(%{
|
||||
first_name: "UnpaidCurrent",
|
||||
membership_fee_type_id: fee_type.id
|
||||
})
|
||||
|
||||
create_cycle(unpaid_member, fee_type, %{cycle_start: current_year_start, status: :unpaid})
|
||||
|
||||
{:ok, _view, html} =
|
||||
live(conn, "/members?cycle_status_filter=unpaid&show_current_cycle=true")
|
||||
|
||||
refute html =~ "PaidCurrent"
|
||||
assert html =~ "UnpaidCurrent"
|
||||
end
|
||||
|
||||
test "toggle cycle view updates URL and preserves filter", %{conn: conn} do
|
||||
conn = conn_with_oidc_user(conn)
|
||||
|
||||
# Start with last cycle view and paid filter
|
||||
{:ok, view, _html} = live(conn, "/members?cycle_status_filter=paid")
|
||||
|
||||
# Toggle to current cycle - this should update URL and preserve filter
|
||||
# Use the button in the membership fee status column header
|
||||
view
|
||||
|> element("button[phx-click='toggle_cycle_view'].btn-xs")
|
||||
|> render_click()
|
||||
|
||||
# Wait for patch to complete
|
||||
path = assert_patch(view)
|
||||
|
||||
# URL should contain both filter and show_current_cycle
|
||||
assert path =~ "cycle_status_filter=paid"
|
||||
assert path =~ "show_current_cycle=true"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue