From 4d6294771a91a2e1dc3884f13a7901ffba99ad85 Mon Sep 17 00:00:00 2001 From: Simon Date: Fri, 10 Jul 2026 16:27:16 +0200 Subject: [PATCH] feat(overview): show suspended cycles separately in the payment tooltip --- lib/mv_web/live/member_live/index.ex | 1 + lib/mv_web/live/member_live/index.html.heex | 47 +++++++++++++++---- .../live/member_live/index/payment_aging.ex | 46 +++++++++--------- priv/gettext/de/LC_MESSAGES/default.po | 1 + priv/gettext/default.pot | 1 + priv/gettext/en/LC_MESSAGES/default.po | 1 + .../index_membership_fee_status_test.exs | 25 ++++++++++ 7 files changed, 91 insertions(+), 31 deletions(-) diff --git a/lib/mv_web/live/member_live/index.ex b/lib/mv_web/live/member_live/index.ex index af0214e7..62a82026 100644 --- a/lib/mv_web/live/member_live/index.ex +++ b/lib/mv_web/live/member_live/index.ex @@ -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 diff --git a/lib/mv_web/live/member_live/index.html.heex b/lib/mv_web/live/member_live/index.html.heex index 0a5b9280..39182330 100644 --- a/lib/mv_web/live/member_live/index.html.heex +++ b/lib/mv_web/live/member_live/index.html.heex @@ -511,8 +511,9 @@ {badge.label} <%= 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) %>
-
<% end %> diff --git a/lib/mv_web/live/member_live/index/payment_aging.ex b/lib/mv_web/live/member_live/index/payment_aging.ex index 644a1597..4f48c326 100644 --- a/lib/mv_web/live/member_live/index/payment_aging.ex +++ b/lib/mv_web/live/member_live/index/payment_aging.ex @@ -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. diff --git a/priv/gettext/de/LC_MESSAGES/default.po b/priv/gettext/de/LC_MESSAGES/default.po index 1a8682ad..5706d57b 100644 --- a/priv/gettext/de/LC_MESSAGES/default.po +++ b/priv/gettext/de/LC_MESSAGES/default.po @@ -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 diff --git a/priv/gettext/default.pot b/priv/gettext/default.pot index ae7e899b..309f5ae4 100644 --- a/priv/gettext/default.pot +++ b/priv/gettext/default.pot @@ -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 diff --git a/priv/gettext/en/LC_MESSAGES/default.po b/priv/gettext/en/LC_MESSAGES/default.po index 9f43b72f..a8decc87 100644 --- a/priv/gettext/en/LC_MESSAGES/default.po +++ b/priv/gettext/en/LC_MESSAGES/default.po @@ -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 diff --git a/test/mv_web/member_live/index_membership_fee_status_test.exs b/test/mv_web/member_live/index_membership_fee_status_test.exs index ae6e5657..f4a2afdc 100644 --- a/test/mv_web/member_live/index_membership_fee_status_test.exs +++ b/test/mv_web/member_live/index_membership_fee_status_test.exs @@ -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})