<%!-- Polite live region: present on first render (before it is filled) so
screen readers announce the exact total matching count on every filter change
(WCAG 4.1.3). --%>
<.live_component
module={MvWeb.Components.SortHeaderComponent}
id={:sort_payment}
field={:payment}
label={MvWeb.MemberLive.Index.payment_column_label(@payment_period)}
sort_hint={gettext("Click to sort by open payment cycles")}
sort_field={@sort_field}
sort_order={@sort_order}
/>
<.badge
:if={MvWeb.MemberLive.Index.payment_period_active?(@payment_period)}
variant="primary"
size="sm"
class="font-normal shrink-0"
title={PaymentAging.period_tooltip(@payment_period)}
aria-label={PaymentAging.period_tooltip(@payment_period)}
data-testid="payment-period-badge"
>
{PaymentAging.period_short_code(@payment_period) || gettext("filtered")}
"""
}
>
<% count = member.unpaid_cycle_count || 0 %>
<% badge = PaymentAging.badge(count) %>
<% tip_id = "payment-tip-#{member.id}" %>
<% anchor = "--pay-#{member.id}" %>
<%!-- The badge is a real navigation link (semantic, keyboard- and
right-click-friendly) into the member's payment history. Its hover/focus
tooltip is a native Popover-API element in the top layer, placed via CSS
anchor positioning (see the `payment-tip` class + the HoverPopover hook),
so it escapes the members table's `overflow` clipping without any custom
coordinate math. No tooltip renders when the member is fully paid. --%>
<.link
navigate={~p"/members/#{member.id}?tab=membership_fees"}
id={"payment-badge-#{member.id}"}
class={["badge badge-soft badge-md", badge.color_class]}
style={count > 0 && "anchor-name: #{anchor}"}
phx-hook={count > 0 && "HoverPopover"}
data-popover-target={count > 0 && tip_id}
aria-details={count > 0 && tip_id}
aria-label={gettext("%{label} — open payment history", label: badge.label)}
data-testid="payment-badge"
>
<.icon name={badge.icon} class="size-4" />
{badge.label}
<%= if count > 0 do %>
<% cycles = PaymentAging.open_cycles(member, @payment_period) %>
<% {shown, overflow} = PaymentAging.tooltip_cycles(cycles) %>
<% end %>
<:col
:let={member}
:if={:groups in @member_fields_visible}
sort_field={:groups}
label={
~H"""
<.live_component
module={MvWeb.Components.SortHeaderComponent}
id={:sort_groups}
field={:groups}
label={gettext("Groups")}
sort_field={@sort_field}
sort_order={@sort_order}
/>
"""
}
>
<.maybe_value value={member.groups} empty_sr_text={gettext("No group assignment")}>
<%= for group <- (member.groups || []) do %>
<.badge
variant="primary"
style="outline"
aria-label={gettext("Member of group %{name}", name: group.name)}
>
{group.name}
<% end %>
<:footer>
<%!-- Invisible sentinel: starts the next-page load as it approaches the
viewport (see the LoadMorePrefetch hook), so rows load ahead of reaching
the very bottom. Only rendered while more pages exist (with the footer). --%>
<%!-- The accent (background + top separator) and row height live on the
full-width footer cell (see #members-footer in app.css) so they span the
whole table edge-to-edge and match the active density. This inner element
stays pinned to the visible viewport width via StickyViewportWidth and is
transparent, so the cell accent shows through while the spinner/label stay
centered during horizontal scroll. --%>
<.spinner size="sm" aria-hidden="true" />
{gettext("Loading more members …")}