feat(overview): replace filter panel with add-filter builder and aging column

Swaps the unbounded vertical filter panel for a Polaris-style add-filter
builder (searchable grouped field picker, type-aware value control, applied
chips) and colour-codes the fees column with each member's period-scoped
unpaid-cycle count.
This commit is contained in:
Simon 2026-07-10 16:27:15 +02:00
parent 7d1a71b1fd
commit c547ca19af
35 changed files with 2817 additions and 3149 deletions

View file

@ -56,8 +56,11 @@ defmodule MvWeb.MemberLive.Index.ExportPayloadTest do
assert payload.query == "ada"
assert payload.sort_field == "last_name"
assert payload.sort_order == "desc"
# Decoupled from the removed overview cycle toggle (§3.3): the export basis
# is always the current cycle and no cycle-status row filter is applied,
# regardless of any legacy cycle assigns.
assert payload.show_current_cycle == true
assert payload.cycle_status_filter == "unpaid"
assert payload.cycle_status_filter == nil
assert payload.boolean_filters == %{"cf-x" => true}
end
@ -79,7 +82,7 @@ defmodule MvWeb.MemberLive.Index.ExportPayloadTest do
assert payload.query == nil
assert payload.sort_field == nil
assert payload.sort_order == nil
assert payload.show_current_cycle == false
assert payload.show_current_cycle == true
assert payload.cycle_status_filter == nil
assert payload.boolean_filters == %{}
end

View file

@ -1,78 +0,0 @@
defmodule MvWeb.MemberLive.Index.OverviewQueryCycleStatusTest do
@moduledoc """
§1.13 the paid/unpaid cycle-status filter, under the current or last-completed
cycle view, returns the same members as the previous in-memory classifier,
computed DB-side via the `cycle_end`-backed aggregates.
"""
use Mv.DataCase, async: false
import Mv.Fixtures, only: [create_fee_type: 2, member_fixture_with_actor: 2, create_cycle: 4]
alias Mv.Membership.Member
alias MvWeb.MemberLive.Index.MembershipFeeStatus
alias MvWeb.MemberLive.Index.OverviewQuery
@today ~D[2024-07-15]
setup do
actor = Mv.Helpers.SystemActor.get_system_actor()
ft = create_fee_type(%{interval: :yearly}, actor)
# current 2024 paid, last-completed 2023 unpaid
paid_now = member_fixture_with_actor(%{membership_fee_type_id: ft.id}, actor)
create_cycle(paid_now, ft, %{cycle_start: ~D[2024-01-01], status: :paid}, actor)
create_cycle(paid_now, ft, %{cycle_start: ~D[2023-01-01], status: :unpaid}, actor)
# current 2024 unpaid, last-completed 2023 paid
unpaid_now = member_fixture_with_actor(%{membership_fee_type_id: ft.id}, actor)
create_cycle(unpaid_now, ft, %{cycle_start: ~D[2024-01-01], status: :unpaid}, actor)
create_cycle(unpaid_now, ft, %{cycle_start: ~D[2023-01-01], status: :paid}, actor)
# no cycles
_no_cycles = member_fixture_with_actor(%{membership_fee_type_id: ft.id}, actor)
%{actor: actor, paid_now: paid_now, unpaid_now: unpaid_now}
end
defp db_ids(status, show_current, actor) do
OverviewQuery.build(%{
cycle_status_filter: status,
show_current_cycle: show_current,
today: @today
})
|> Ash.read!(actor: actor)
|> MapSet.new(& &1.id)
end
defp oracle_ids(status, show_current, actor) do
Member
|> Ash.Query.load(membership_fee_cycles: [:membership_fee_type])
|> Ash.Query.load(:membership_fee_type)
|> Ash.read!(actor: actor)
|> Enum.filter(fn m ->
MembershipFeeStatus.get_cycle_status_for_member(m, show_current, @today) == status
end)
|> MapSet.new(& &1.id)
end
# {status, show_current, member-key expected in the result}
@cases [
{:paid, true, :paid_now},
{:unpaid, true, :unpaid_now},
{:paid, false, :unpaid_now},
{:unpaid, false, :paid_now}
]
for {status, show_current, expected_key} <- @cases do
test "#{status} under #{if show_current, do: "current", else: "last"} cycle matches oracle",
%{actor: actor} = ctx do
status = unquote(status)
show_current = unquote(show_current)
expected = Map.fetch!(ctx, unquote(expected_key))
db = db_ids(status, show_current, actor)
assert db == oracle_ids(status, show_current, actor)
assert expected.id in db
end
end
end