Rework Filter using a filter builder #556
7 changed files with 91 additions and 31 deletions
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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.yyyy–dd.MM.yyyy` date range when the interval is not
|
Falls back to a `dd.MM.yyyy–dd.MM.yyyy` date range when the interval is not
|
||||||
loaded/known.
|
loaded/known.
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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})
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue