Rework Filter using a filter builder #556

Open
simon wants to merge 11 commits from issue/mitgliederverwaltung-548 into issue/mitgliederverwaltung-547
7 changed files with 91 additions and 31 deletions
Showing only changes of commit 4d6294771a - Show all commits

View file

@ -49,6 +49,7 @@ defmodule MvWeb.MemberLive.Index do
alias Mv.MembershipFees alias Mv.MembershipFees
alias Mv.MembershipFees.MembershipFeeType alias Mv.MembershipFees.MembershipFeeType
alias MvWeb.Helpers.DateFormatter alias MvWeb.Helpers.DateFormatter
alias MvWeb.Helpers.MembershipFeeHelpers
alias MvWeb.MemberLive.Index.AsOfDate alias MvWeb.MemberLive.Index.AsOfDate
alias MvWeb.MemberLive.Index.CustomFieldValueLookup alias MvWeb.MemberLive.Index.CustomFieldValueLookup
alias MvWeb.MemberLive.Index.DateFilter alias MvWeb.MemberLive.Index.DateFilter

View file

@ -511,8 +511,9 @@
{badge.label} {badge.label}
</.link> </.link>
<%= if count > 0 do %> <%= if count > 0 do %>
<% cycles = PaymentAging.open_cycles(member, @payment_period) %> <% %{unpaid: unpaid, suspended: suspended} =
<% {shown, overflow} = PaymentAging.tooltip_cycles(cycles) %> PaymentAging.partition_open_cycles(member, @payment_period) %>
<% {shown, overflow} = PaymentAging.tooltip_cycles(unpaid) %>
<div <div
id={tip_id} id={tip_id}
popover="manual" popover="manual"
@ -521,15 +522,21 @@
class="payment-tip m-0 w-fit rounded-box border border-base-300 bg-base-100 p-2 text-sm shadow-xl" class="payment-tip m-0 w-fit rounded-box border border-base-300 bg-base-100 p-2 text-sm shadow-xl"
data-testid="payment-tooltip" data-testid="payment-tooltip"
> >
<ul class="m-0 flex list-none flex-col items-stretch gap-1 p-0"> <ul
class="m-0 flex list-none flex-col items-stretch gap-1 p-0"
data-testid="payment-tooltip-unpaid"
>
<li :for={cycle <- shown} class="flex"> <li :for={cycle <- shown} class="flex">
<span class={[ <.badge
"badge badge-soft badge-sm w-full justify-start", variant={MembershipFeeHelpers.status_variant(cycle.status)}
PaymentAging.cycle_color_class(cycle.status) size="sm"
]}> class="w-full justify-start"
<.icon name={PaymentAging.cycle_icon(cycle.status)} class="size-3.5" /> >
<:icon>
<.icon name={MembershipFeeHelpers.status_icon(cycle.status)} class="size-3.5" />
</:icon>
{PaymentAging.short_period(cycle)} {PaymentAging.short_period(cycle)}
</span> </.badge>
</li> </li>
<li :if={overflow > 0} class="flex"> <li :if={overflow > 0} class="flex">
<span class="badge badge-soft badge-sm badge-ghost w-full justify-start"> <span class="badge badge-soft badge-sm badge-ghost w-full justify-start">
@ -538,6 +545,28 @@
</span> </span>
</li> </li>
</ul> </ul>
<%!-- Suspended cycles are shown separately under their own label so
the status distinction is not color-only (§1.14). --%>
<div :if={suspended != []} class="mt-2" data-testid="payment-tooltip-suspended">
<p class="mb-1 text-xs font-medium opacity-70">{gettext("Suspended")}</p>
<ul class="m-0 flex list-none flex-col items-stretch gap-1 p-0">
<li :for={cycle <- suspended} class="flex">
<.badge
variant={MembershipFeeHelpers.status_variant(cycle.status)}
size="sm"
class="w-full justify-start"
>
<:icon>
<.icon
name={MembershipFeeHelpers.status_icon(cycle.status)}
class="size-3.5"
/>
</:icon>
{PaymentAging.short_period(cycle)}
</.badge>
</li>
</ul>
</div>
</div> </div>
<% end %> <% end %>
</:col> </:col>

View file

@ -272,14 +272,32 @@ defmodule MvWeb.MemberLive.Index.PaymentAging do
|> Enum.sort_by(& &1.cycle_start, Date) |> Enum.sort_by(& &1.cycle_start, Date)
end end
@doc """
Partitions a member's open cycles for the period into the unpaid cycles (which
drive the count and the badge) and the suspended cycles, which the tooltip
surfaces in their own labeled section rather than by color alone (§1.14).
Both lists stay in chronological order.
"""
@spec partition_open_cycles(map(), period()) :: %{unpaid: [map()], suspended: [map()]}
def partition_open_cycles(member, period) do
{unpaid, suspended} =
member
|> open_cycles(period)
|> Enum.split_with(&(&1.status == :unpaid))
%{unpaid: unpaid, suspended: suspended}
end
@tooltip_slots 5 @tooltip_slots 5
@doc """ @doc """
Partitions open cycles into the tooltip's fixed five-slot budget: at most Partitions the tooltip's unpaid sub-list into a fixed five-slot budget: at
five badges total. Five or fewer cycles show in full with no overflow. More most five badges for the unpaid cycles. Five or fewer unpaid cycles show in
than five collapse to the first four chronological cycles plus a single full with no overflow. More than five collapse to the first four chronological
overflow count (total 4), so the tooltip renders four cycle badges and one cycles plus a single overflow count (total 4), so the unpaid sub-list
grey overflow badge five items. renders four cycle badges and one grey overflow badge five items. Suspended
cycles are listed separately (in their own labeled section) and are not capped
by this budget, so the tooltip's overall badge count can exceed five.
""" """
@spec tooltip_cycles([map()]) :: {[map()], non_neg_integer()} @spec tooltip_cycles([map()]) :: {[map()], non_neg_integer()}
def tooltip_cycles(cycles) when is_list(cycles) do def tooltip_cycles(cycles) when is_list(cycles) do
@ -290,22 +308,6 @@ defmodule MvWeb.MemberLive.Index.PaymentAging do
end end
end end
@doc """
Static daisyUI badge color token for a cycle status, matching the codes used
on the member show page. Literal strings so Tailwind's content scanner keeps
them in the bundle (a runtime `badge-\#{status}` would be tree-shaken).
"""
@spec cycle_color_class(:paid | :unpaid | :suspended) :: String.t()
def cycle_color_class(:paid), do: "badge-success"
def cycle_color_class(:unpaid), do: "badge-error"
def cycle_color_class(:suspended), do: "badge-warning"
@doc """
Hero-icon name for a cycle status (reuses the established status icons).
"""
@spec cycle_icon(:paid | :unpaid | :suspended) :: String.t()
def cycle_icon(status), do: MembershipFeeHelpers.status_icon(status)
@doc """ @doc """
Short, human-readable period label for a cycle, derived from its `cycle_start` Short, human-readable period label for a cycle, derived from its `cycle_start`
and the fee-type interval: and the fee-type interval:
@ -313,7 +315,7 @@ defmodule MvWeb.MemberLive.Index.PaymentAging do
* yearly `"2025"` * yearly `"2025"`
* quarterly `"Q1 2026"` * quarterly `"Q1 2026"`
* half-yearly `"H1 2026"` * half-yearly `"H1 2026"`
* monthly `"März 2026"` * monthly `"March 2026"`
Falls back to a `dd.MM.yyyydd.MM.yyyy` date range when the interval is not Falls back to a `dd.MM.yyyydd.MM.yyyy` date range when the interval is not
loaded/known. loaded/known.

View file

@ -3259,6 +3259,7 @@ msgid "Summary"
msgstr "Zusammenfassung" msgstr "Zusammenfassung"
#: lib/mv/membership/members_pdf.ex #: lib/mv/membership/members_pdf.ex
#: lib/mv_web/live/member_live/index.html.heex
#: lib/mv_web/live/member_live/index/membership_fee_status.ex #: lib/mv_web/live/member_live/index/membership_fee_status.ex
#: lib/mv_web/live/member_live/show.ex #: lib/mv_web/live/member_live/show.ex
#: lib/mv_web/live/member_live/show/membership_fees_component.ex #: lib/mv_web/live/member_live/show/membership_fees_component.ex

View file

@ -3260,6 +3260,7 @@ msgid "Summary"
msgstr "" msgstr ""
#: lib/mv/membership/members_pdf.ex #: lib/mv/membership/members_pdf.ex
#: lib/mv_web/live/member_live/index.html.heex
#: lib/mv_web/live/member_live/index/membership_fee_status.ex #: lib/mv_web/live/member_live/index/membership_fee_status.ex
#: lib/mv_web/live/member_live/show.ex #: lib/mv_web/live/member_live/show.ex
#: lib/mv_web/live/member_live/show/membership_fees_component.ex #: lib/mv_web/live/member_live/show/membership_fees_component.ex

View file

@ -3260,6 +3260,7 @@ msgid "Summary"
msgstr "" msgstr ""
#: lib/mv/membership/members_pdf.ex #: lib/mv/membership/members_pdf.ex
#: lib/mv_web/live/member_live/index.html.heex
#: lib/mv_web/live/member_live/index/membership_fee_status.ex #: lib/mv_web/live/member_live/index/membership_fee_status.ex
#: lib/mv_web/live/member_live/show.ex #: lib/mv_web/live/member_live/show.ex
#: lib/mv_web/live/member_live/show/membership_fees_component.ex #: lib/mv_web/live/member_live/show/membership_fees_component.ex

View file

@ -83,6 +83,31 @@ defmodule MvWeb.MemberLive.IndexMembershipFeeStatusTest do
assert has_element?(view, "#payment-tip-#{member.id}[popover]") assert has_element?(view, "#payment-tip-#{member.id}[popover]")
end end
test "the tooltip lists suspended cycles in a separate labeled section (§1.14)", %{conn: conn} do
fee_type = create_fee_type(%{interval: :yearly})
member = create_member(%{first_name: "Mixed", membership_fee_type_id: fee_type.id})
create_cycle(member, fee_type, %{cycle_start: ~D[2023-01-01], status: :unpaid})
create_cycle(member, fee_type, %{cycle_start: ~D[2024-01-01], status: :suspended})
{:ok, view, _html} = live(conn, "/members")
tip = "#payment-tip-#{member.id}"
# Suspended cycles are shown separately, under their own text label — the
# distinction is not color-only. The suspended cycle's period lives in the
# suspended section and is absent from the unpaid list.
assert has_element?(view, "#{tip} [data-testid='payment-tooltip-suspended']", "Suspended")
suspended =
view |> element("#{tip} [data-testid='payment-tooltip-suspended']") |> render()
assert suspended =~ "2024"
unpaid = view |> element("#{tip} [data-testid='payment-tooltip-unpaid']") |> render()
assert unpaid =~ "2023"
refute unpaid =~ "2024"
end
test "a fully-paid member renders the badge but no tooltip popover", %{conn: conn} do test "a fully-paid member renders the badge but no tooltip popover", %{conn: conn} do
fee_type = create_fee_type(%{interval: :yearly}) fee_type = create_fee_type(%{interval: :yearly})
member = create_member(%{first_name: "Clean", membership_fee_type_id: fee_type.id}) member = create_member(%{first_name: "Clean", membership_fee_type_id: fee_type.id})