feat: add membership fee status column to member list

- Add status column showing last completed or current cycle status
- Add toggle to switch between last/current cycle view
- Add color coding (green/red/gray) for paid/unpaid/suspended
- Add filters for unpaid cycles in last/current cycle
- Efficiently load cycles to avoid N+1 queries
This commit is contained in:
Moritz 2025-12-16 10:57:48 +01:00
parent 06de9d2c8b
commit 99dc17bf4d
Signed by: moritz
GPG key ID: 1020A035E5DD0824
2 changed files with 240 additions and 35 deletions

View file

@ -42,6 +42,66 @@
paid_filter={@paid_filter}
member_count={length(@members)}
/>
<div class="flex gap-2 items-center">
<button
type="button"
phx-click="toggle_cycle_view"
class={[
"btn btn-sm",
if(@show_current_cycle, do: "btn-primary", else: "btn-outline")
]}
aria-label={
if(@show_current_cycle,
do: gettext("Show last completed cycle"),
else: gettext("Show current cycle")
)
}
>
<.icon name="hero-arrow-path" class="size-4" />
{if(@show_current_cycle, do: gettext("Current Cycle"), else: gettext("Last Cycle"))}
</button>
<div class="dropdown">
<label tabindex="0" class="btn btn-sm btn-outline">
<.icon name="hero-funnel" class="size-4" />
{gettext("Membership Fee")}
</label>
<ul
tabindex="0"
class="dropdown-content menu bg-base-100 rounded-box z-[1] w-52 p-2 shadow"
>
<li>
<button
type="button"
phx-click="filter_unpaid_cycles"
phx-value-filter="unpaid_last"
class={if(@membership_fee_status_filter == :unpaid_last, do: "active", else: "")}
>
{gettext("Unpaid in last cycle")}
</button>
</li>
<li>
<button
type="button"
phx-click="filter_unpaid_cycles"
phx-value-filter="unpaid_current"
class={if(@membership_fee_status_filter == :unpaid_current, do: "active", else: "")}
>
{gettext("Unpaid in current cycle")}
</button>
</li>
<li>
<button
type="button"
phx-click="filter_unpaid_cycles"
phx-value-filter=""
class={if(@membership_fee_status_filter == nil, do: "active", else: "")}
>
{gettext("All")}
</button>
</li>
</ul>
</div>
</div>
<.live_component
module={MvWeb.Components.FieldVisibilityDropdownComponent}
id="field-visibility-dropdown"
@ -255,6 +315,40 @@
{if member.paid == true, do: gettext("Yes"), else: gettext("No")}
</span>
</:col>
<:col
:let={member}
label={
~H"""
<div class="flex items-center gap-2">
<span>{gettext("Membership Fee Status")}</span>
<button
type="button"
phx-click="toggle_cycle_view"
class="btn btn-xs btn-ghost"
title={
if(@show_current_cycle,
do: gettext("Switch to last completed cycle"),
else: gettext("Switch to current cycle")
)
}
>
<.icon name="hero-arrow-path" class="size-3" />
</button>
</div>
"""
}
>
<%= if badge = MvWeb.MemberLive.Index.MembershipFeeStatus.format_cycle_status_badge(
MvWeb.MemberLive.Index.MembershipFeeStatus.get_cycle_status_for_member(member, @show_current_cycle)
) do %>
<span class={["badge", badge.color]}>
<.icon name={badge.icon} class="size-4" />
{badge.label}
</span>
<% else %>
<span class="badge badge-ghost">{gettext("No cycle")}</span>
<% end %>
</:col>
<:action :let={member}>
<div class="sr-only">
<.link navigate={~p"/members/#{member}"}>{gettext("Show")}</.link>