feat(overview): show suspended cycles separately in the payment tooltip
This commit is contained in:
parent
e967cb59e9
commit
4d6294771a
7 changed files with 91 additions and 31 deletions
|
|
@ -49,6 +49,7 @@ defmodule MvWeb.MemberLive.Index do
|
|||
alias Mv.MembershipFees
|
||||
alias Mv.MembershipFees.MembershipFeeType
|
||||
alias MvWeb.Helpers.DateFormatter
|
||||
alias MvWeb.Helpers.MembershipFeeHelpers
|
||||
alias MvWeb.MemberLive.Index.AsOfDate
|
||||
alias MvWeb.MemberLive.Index.CustomFieldValueLookup
|
||||
alias MvWeb.MemberLive.Index.DateFilter
|
||||
|
|
|
|||
|
|
@ -511,8 +511,9 @@
|
|||
{badge.label}
|
||||
</.link>
|
||||
<%= if count > 0 do %>
|
||||
<% cycles = PaymentAging.open_cycles(member, @payment_period) %>
|
||||
<% {shown, overflow} = PaymentAging.tooltip_cycles(cycles) %>
|
||||
<% %{unpaid: unpaid, suspended: suspended} =
|
||||
PaymentAging.partition_open_cycles(member, @payment_period) %>
|
||||
<% {shown, overflow} = PaymentAging.tooltip_cycles(unpaid) %>
|
||||
<div
|
||||
id={tip_id}
|
||||
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"
|
||||
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">
|
||||
<span class={[
|
||||
"badge badge-soft badge-sm w-full justify-start",
|
||||
PaymentAging.cycle_color_class(cycle.status)
|
||||
]}>
|
||||
<.icon name={PaymentAging.cycle_icon(cycle.status)} class="size-3.5" />
|
||||
<.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)}
|
||||
</span>
|
||||
</.badge>
|
||||
</li>
|
||||
<li :if={overflow > 0} class="flex">
|
||||
<span class="badge badge-soft badge-sm badge-ghost w-full justify-start">
|
||||
|
|
@ -538,6 +545,28 @@
|
|||
</span>
|
||||
</li>
|
||||
</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>
|
||||
<% end %>
|
||||
</:col>
|
||||
|
|
|
|||
|
|
@ -272,14 +272,32 @@ defmodule MvWeb.MemberLive.Index.PaymentAging do
|
|||
|> Enum.sort_by(& &1.cycle_start, Date)
|
||||
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
|
||||
|
||||
@doc """
|
||||
Partitions open cycles into the tooltip's fixed five-slot budget: at most
|
||||
five badges total. Five or fewer cycles show in full with no overflow. More
|
||||
than five collapse to the first four chronological cycles plus a single
|
||||
overflow count (total − 4), so the tooltip renders four cycle badges and one
|
||||
grey overflow badge — five items.
|
||||
Partitions the tooltip's unpaid sub-list into a fixed five-slot budget: at
|
||||
most five badges for the unpaid cycles. Five or fewer unpaid cycles show in
|
||||
full with no overflow. More than five collapse to the first four chronological
|
||||
cycles plus a single overflow count (total − 4), so the unpaid sub-list
|
||||
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()}
|
||||
def tooltip_cycles(cycles) when is_list(cycles) do
|
||||
|
|
@ -290,22 +308,6 @@ defmodule MvWeb.MemberLive.Index.PaymentAging do
|
|||
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 """
|
||||
Short, human-readable period label for a cycle, derived from its `cycle_start`
|
||||
and the fee-type interval:
|
||||
|
|
@ -313,7 +315,7 @@ defmodule MvWeb.MemberLive.Index.PaymentAging do
|
|||
* yearly → `"2025"`
|
||||
* quarterly → `"Q1 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
|
||||
loaded/known.
|
||||
|
|
|
|||
|
|
@ -3259,6 +3259,7 @@ msgid "Summary"
|
|||
msgstr "Zusammenfassung"
|
||||
|
||||
#: 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/show.ex
|
||||
#: lib/mv_web/live/member_live/show/membership_fees_component.ex
|
||||
|
|
|
|||
|
|
@ -3260,6 +3260,7 @@ msgid "Summary"
|
|||
msgstr ""
|
||||
|
||||
#: 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/show.ex
|
||||
#: lib/mv_web/live/member_live/show/membership_fees_component.ex
|
||||
|
|
|
|||
|
|
@ -3260,6 +3260,7 @@ msgid "Summary"
|
|||
msgstr ""
|
||||
|
||||
#: 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/show.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]")
|
||||
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
|
||||
fee_type = create_fee_type(%{interval: :yearly})
|
||||
member = create_member(%{first_name: "Clean", membership_fee_type_id: fee_type.id})
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue