The growing row of bulk-action buttons above the member overview is replaced by one "Aktionen" dropdown holding all four actions (open in email program, copy addresses, export CSV, export PDF). With no selection the actions operate on all — or the currently filtered — members; the email-program action is disabled past a recipient cap, because the browser cannot reliably hand a very long mailto over to the mail client. The trigger shows the active scope as a badge: an emphasized count when members are selected, a muted "alle"/"gefiltert" otherwise.
408 lines
12 KiB
Text
408 lines
12 KiB
Text
<Layouts.app flash={@flash} current_user={@current_user}>
|
|
<.header>
|
|
{@content_title}
|
|
<:actions>
|
|
<.live_component
|
|
module={MvWeb.Components.BulkActionsDropdown}
|
|
id="bulk-actions-dropdown"
|
|
export_payload_json={@export_payload_json}
|
|
selected_count={@selected_count}
|
|
scope={@scope}
|
|
mailto_bcc={@mailto_bcc}
|
|
recipient_count={@recipient_count}
|
|
mailto_disabled?={@mailto_disabled?}
|
|
/>
|
|
<%= if can?(@current_user, :create, Mv.Membership.Member) do %>
|
|
<.button variant="primary" navigate={~p"/members/new"} data-testid="member-new">
|
|
<.icon name="hero-plus" /> {gettext("New Member")}
|
|
</.button>
|
|
<% end %>
|
|
</:actions>
|
|
</.header>
|
|
|
|
<div class="flex flex-wrap gap-4 items-center">
|
|
<.live_component
|
|
module={MvWeb.Components.SearchBarComponent}
|
|
id="search-bar"
|
|
query={@query}
|
|
placeholder={gettext("Search...")}
|
|
/>
|
|
<.live_component
|
|
module={MvWeb.Components.MemberFilterComponent}
|
|
id="member-filter"
|
|
cycle_status_filter={@cycle_status_filter}
|
|
groups={@groups}
|
|
group_filters={@group_filters}
|
|
fee_types={@fee_types}
|
|
fee_type_filters={@fee_type_filters}
|
|
boolean_custom_fields={@boolean_custom_fields}
|
|
boolean_filters={@boolean_custom_field_filters}
|
|
date_custom_fields={@date_custom_fields}
|
|
date_filters={@date_filters}
|
|
member_count={length(@members)}
|
|
/>
|
|
<.tooltip
|
|
content={
|
|
gettext(
|
|
"Sets whether the payment status filter and the membership fee status column use the last completed or the current payment cycle."
|
|
)
|
|
}
|
|
position="top"
|
|
>
|
|
<.button
|
|
type="button"
|
|
variant="secondary"
|
|
class={["gap-2", @show_current_cycle && "btn-active"]}
|
|
phx-click="toggle_cycle_view"
|
|
data-testid="toggle-cycle-view"
|
|
aria-label={
|
|
if(@show_current_cycle,
|
|
do: gettext("Current payment cycle"),
|
|
else: gettext("Last payment cycle")
|
|
)
|
|
}
|
|
>
|
|
<.icon name="hero-arrow-path" class="h-5 w-5" />
|
|
<span class="hidden sm:inline">
|
|
{if(@show_current_cycle,
|
|
do: gettext("Current payment cycle"),
|
|
else: gettext("Last payment cycle")
|
|
)}
|
|
</span>
|
|
</.button>
|
|
</.tooltip>
|
|
<.live_component
|
|
module={MvWeb.Components.FieldVisibilityDropdownComponent}
|
|
id="field-visibility-dropdown"
|
|
all_fields={@all_available_fields}
|
|
custom_fields={@all_custom_fields}
|
|
selected_fields={@user_field_selection}
|
|
/>
|
|
</div>
|
|
|
|
<%!-- On desktop (lg:), only the table area scrolls; header and filters stay visible. On mobile, normal flow. --%>
|
|
<div
|
|
class="lg:max-h-[calc(100vh-14rem)] lg:overflow-auto min-h-0"
|
|
data-testid="members-table-scroll"
|
|
role="region"
|
|
aria-label={gettext("Members table")}
|
|
>
|
|
<.table
|
|
id="members"
|
|
rows={@members}
|
|
wrapper_overflow_class="overflow-visible"
|
|
sticky_header={true}
|
|
sticky_first_col={true}
|
|
row_id={fn member -> "row-#{member.id}" end}
|
|
row_click={fn member -> JS.push("select_row_and_navigate", value: %{id: member.id}) end}
|
|
row_tooltip={gettext("Click for member details")}
|
|
row_selected?={fn member -> MapSet.member?(@selected_members, member.id) end}
|
|
dynamic_cols={@dynamic_cols}
|
|
sort_field={@sort_field}
|
|
sort_order={@sort_order}
|
|
>
|
|
|
|
<!-- <:col :let={member} label="Id">{member.id}</:col> -->
|
|
<:col
|
|
:let={member}
|
|
col_click={&MvWeb.MemberLive.Index.checkbox_column_click/1}
|
|
label={
|
|
~H"""
|
|
<.input
|
|
type="checkbox"
|
|
name="select_all"
|
|
phx-click="select_all"
|
|
checked={MapSet.equal?(@selected_members, @members |> Enum.map(& &1.id) |> MapSet.new())}
|
|
aria-label={gettext("Select all members")}
|
|
role="checkbox"
|
|
/>
|
|
"""
|
|
}
|
|
>
|
|
<.input
|
|
type="checkbox"
|
|
name={member.id}
|
|
checked={MapSet.member?(@selected_members, member.id)}
|
|
aria-label={gettext("Select member")}
|
|
role="checkbox"
|
|
/>
|
|
</:col>
|
|
<:col
|
|
:let={member}
|
|
:if={:first_name in @member_fields_visible}
|
|
label={
|
|
~H"""
|
|
<.live_component
|
|
module={MvWeb.Components.SortHeaderComponent}
|
|
id={:sort_first_name}
|
|
field={:first_name}
|
|
label={gettext("First name")}
|
|
sort_field={@sort_field}
|
|
sort_order={@sort_order}
|
|
/>
|
|
"""
|
|
}
|
|
>
|
|
{member.first_name}
|
|
</:col>
|
|
<:col
|
|
:let={member}
|
|
:if={:last_name in @member_fields_visible}
|
|
label={
|
|
~H"""
|
|
<.live_component
|
|
module={MvWeb.Components.SortHeaderComponent}
|
|
id={:sort_last_name}
|
|
field={:last_name}
|
|
label={gettext("Last name")}
|
|
sort_field={@sort_field}
|
|
sort_order={@sort_order}
|
|
/>
|
|
"""
|
|
}
|
|
>
|
|
{member.last_name}
|
|
</:col>
|
|
<:col
|
|
:let={member}
|
|
:if={:email in @member_fields_visible}
|
|
label={
|
|
~H"""
|
|
<.live_component
|
|
module={MvWeb.Components.SortHeaderComponent}
|
|
id={:sort_email}
|
|
field={:email}
|
|
label={gettext("Email")}
|
|
sort_field={@sort_field}
|
|
sort_order={@sort_order}
|
|
/>
|
|
"""
|
|
}
|
|
>
|
|
{member.email}
|
|
</:col>
|
|
<:col
|
|
:let={member}
|
|
:if={:join_date in @member_fields_visible}
|
|
label={
|
|
~H"""
|
|
<.live_component
|
|
module={MvWeb.Components.SortHeaderComponent}
|
|
id={:sort_join_date}
|
|
field={:join_date}
|
|
label={gettext("Join Date")}
|
|
sort_field={@sort_field}
|
|
sort_order={@sort_order}
|
|
/>
|
|
"""
|
|
}
|
|
>
|
|
{MvWeb.MemberLive.Index.format_date(member.join_date)}
|
|
</:col>
|
|
<:col
|
|
:let={member}
|
|
:if={:exit_date in @member_fields_visible}
|
|
label={
|
|
~H"""
|
|
<.live_component
|
|
module={MvWeb.Components.SortHeaderComponent}
|
|
id={:sort_exit_date}
|
|
field={:exit_date}
|
|
label={gettext("Exit Date")}
|
|
sort_field={@sort_field}
|
|
sort_order={@sort_order}
|
|
/>
|
|
"""
|
|
}
|
|
>
|
|
{MvWeb.MemberLive.Index.format_date(member.exit_date)}
|
|
</:col>
|
|
<:col
|
|
:let={member}
|
|
:if={:notes in @member_fields_visible}
|
|
label={gettext("Notes")}
|
|
>
|
|
{member.notes}
|
|
</:col>
|
|
<:col
|
|
:let={member}
|
|
:if={:country in @member_fields_visible}
|
|
label={
|
|
~H"""
|
|
<.live_component
|
|
module={MvWeb.Components.SortHeaderComponent}
|
|
id={:sort_country}
|
|
field={:country}
|
|
label={gettext("Country")}
|
|
sort_field={@sort_field}
|
|
sort_order={@sort_order}
|
|
/>
|
|
"""
|
|
}
|
|
>
|
|
{member.country}
|
|
</:col>
|
|
<:col
|
|
:let={member}
|
|
:if={:city in @member_fields_visible}
|
|
label={
|
|
~H"""
|
|
<.live_component
|
|
module={MvWeb.Components.SortHeaderComponent}
|
|
id={:sort_city}
|
|
field={:city}
|
|
label={gettext("City")}
|
|
sort_field={@sort_field}
|
|
sort_order={@sort_order}
|
|
/>
|
|
"""
|
|
}
|
|
>
|
|
{member.city}
|
|
</:col>
|
|
<:col
|
|
:let={member}
|
|
:if={:street in @member_fields_visible}
|
|
label={
|
|
~H"""
|
|
<.live_component
|
|
module={MvWeb.Components.SortHeaderComponent}
|
|
id={:sort_street}
|
|
field={:street}
|
|
label={gettext("Street")}
|
|
sort_field={@sort_field}
|
|
sort_order={@sort_order}
|
|
/>
|
|
"""
|
|
}
|
|
>
|
|
{member.street}
|
|
</:col>
|
|
<:col
|
|
:let={member}
|
|
:if={:house_number in @member_fields_visible}
|
|
label={
|
|
~H"""
|
|
<.live_component
|
|
module={MvWeb.Components.SortHeaderComponent}
|
|
id={:sort_house_number}
|
|
field={:house_number}
|
|
label={gettext("House Number")}
|
|
sort_field={@sort_field}
|
|
sort_order={@sort_order}
|
|
/>
|
|
"""
|
|
}
|
|
>
|
|
{member.house_number}
|
|
</:col>
|
|
<:col
|
|
:let={member}
|
|
:if={:postal_code in @member_fields_visible}
|
|
label={
|
|
~H"""
|
|
<.live_component
|
|
module={MvWeb.Components.SortHeaderComponent}
|
|
id={:sort_postal_code}
|
|
field={:postal_code}
|
|
label={gettext("Postal Code")}
|
|
sort_field={@sort_field}
|
|
sort_order={@sort_order}
|
|
/>
|
|
"""
|
|
}
|
|
>
|
|
{member.postal_code}
|
|
</:col>
|
|
<:col
|
|
:let={member}
|
|
:if={:membership_fee_start_date in @member_fields_visible}
|
|
label={
|
|
~H"""
|
|
<.live_component
|
|
module={MvWeb.Components.SortHeaderComponent}
|
|
id={:sort_membership_fee_start_date}
|
|
field={:membership_fee_start_date}
|
|
label={gettext("Membership Fee Start Date")}
|
|
sort_field={@sort_field}
|
|
sort_order={@sort_order}
|
|
/>
|
|
"""
|
|
}
|
|
>
|
|
{MvWeb.MemberLive.Index.format_date(member.membership_fee_start_date)}
|
|
</:col>
|
|
<:col
|
|
:let={member}
|
|
:if={:membership_fee_type in @member_fields_visible}
|
|
label={
|
|
~H"""
|
|
<.live_component
|
|
module={MvWeb.Components.SortHeaderComponent}
|
|
id={:sort_membership_fee_type}
|
|
field={:membership_fee_type}
|
|
label={gettext("Fee Type")}
|
|
sort_field={@sort_field}
|
|
sort_order={@sort_order}
|
|
/>
|
|
"""
|
|
}
|
|
>
|
|
<.maybe_value value={member.membership_fee_type} empty_sr_text={gettext("Not specified")}>
|
|
{member.membership_fee_type.name}
|
|
</.maybe_value>
|
|
</:col>
|
|
<:col
|
|
:let={member}
|
|
:if={:membership_fee_status in @member_fields_visible}
|
|
label={gettext("Membership Fee Status")}
|
|
>
|
|
<%= if badge = MembershipFeeStatus.format_cycle_status_badge(
|
|
MembershipFeeStatus.get_cycle_status_for_member(member, @show_current_cycle)
|
|
) do %>
|
|
<.badge variant={badge.variant}>
|
|
<.icon name={badge.icon} class="size-4" />
|
|
{badge.label}
|
|
</.badge>
|
|
<% else %>
|
|
<.empty_cell sr_text={gettext("No cycle")} />
|
|
<% end %>
|
|
</:col>
|
|
<:col
|
|
:let={member}
|
|
:if={:groups in @member_fields_visible}
|
|
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}
|
|
</.badge>
|
|
<% end %>
|
|
</.maybe_value>
|
|
</:col>
|
|
<:action :let={member}>
|
|
<div class="sr-only">
|
|
<.link navigate={~p"/members/#{member}"} data-testid="member-show-link">
|
|
{gettext("Show")}
|
|
</.link>
|
|
</div>
|
|
</:action>
|
|
</.table>
|
|
</div>
|
|
</Layouts.app>
|