@@ -361,18 +274,6 @@ defmodule MvWeb.Components.MemberFilterComponent do
_ -> nil
end
- # Parse per-group filters (params keys "group_
" => "all"|"in"|"not_in")
- prefix_len = String.length(@group_filter_prefix)
-
- group_filters_parsed =
- params
- |> Enum.filter(fn {key, _} -> String.starts_with?(key, @group_filter_prefix) end)
- |> Enum.reduce(%{}, fn {key, value_str}, acc ->
- group_id_str = String.slice(key, prefix_len, String.length(key) - prefix_len)
- filter_value = parse_group_filter_value(value_str)
- Map.put(acc, group_id_str, filter_value)
- end)
-
# Parse boolean custom field filters (including nil values for "all")
custom_boolean_filters_parsed =
params
@@ -387,20 +288,6 @@ defmodule MvWeb.Components.MemberFilterComponent do
send(self(), {:payment_filter_changed, payment_filter})
end
- # Update group filters - send event for each changed group
- current_group_filters = socket.assigns.group_filters
- all_group_ids = MapSet.new(Enum.map(socket.assigns.groups, &to_string(&1.id)))
-
- Enum.each(group_filters_parsed, fn {group_id_str, new_value} ->
- in_set = MapSet.member?(all_group_ids, group_id_str)
- current_value = Map.get(current_group_filters, group_id_str)
- should_send = in_set and current_value != new_value
-
- if should_send do
- send(self(), {:group_filter_changed, group_id_str, new_value})
- end
- end)
-
# Update boolean filters - send events for each changed filter
current_filters = socket.assigns.boolean_filters
@@ -423,7 +310,7 @@ defmodule MvWeb.Components.MemberFilterComponent do
def handle_event("reset_filters", _params, socket) do
# Send single message to reset all filters at once (performance optimization)
# This avoids N×2 load_members() calls when resetting multiple filters
- send(self(), {:reset_all_filters, nil, %{}, %{}})
+ send(self(), {:reset_all_filters, nil, %{}})
# Close dropdown after reset
{:noreply, assign(socket, :open, false)}
@@ -435,48 +322,17 @@ defmodule MvWeb.Components.MemberFilterComponent do
defp parse_tri_state("all"), do: nil
defp parse_tri_state(_), do: nil
- defp parse_group_filter_value("in"), do: :in
- defp parse_group_filter_value("not_in"), do: :not_in
- defp parse_group_filter_value(_), do: nil
-
# Get display label for button
- defp button_label(
- cycle_status_filter,
- groups,
- group_filters,
- boolean_custom_fields,
- boolean_filters
- ) do
- cond do
- cycle_status_filter ->
- payment_filter_label(cycle_status_filter)
-
- map_size(group_filters) > 0 ->
- group_filters_label(groups, group_filters)
-
- map_size(boolean_filters) > 0 ->
- boolean_filter_label(boolean_custom_fields, boolean_filters)
-
- true ->
- gettext("All")
+ defp button_label(cycle_status_filter, boolean_custom_fields, boolean_filters) do
+ # If payment filter is active, show payment filter label
+ if cycle_status_filter do
+ payment_filter_label(cycle_status_filter)
+ else
+ # Otherwise show boolean filter labels
+ boolean_filter_label(boolean_custom_fields, boolean_filters)
end
end
- defp group_filters_label(_groups, group_filters) when map_size(group_filters) == 0,
- do: gettext("All")
-
- defp group_filters_label(groups, group_filters) do
- groups_by_id = Map.new(groups, fn g -> {to_string(g.id), g.name} end)
-
- names =
- group_filters
- |> Enum.map(fn {group_id_str, _} -> Map.get(groups_by_id, group_id_str) end)
- |> Enum.reject(&is_nil/1)
-
- label = Enum.join(names, ", ")
- truncate_label(label, 30)
- end
-
# Get payment filter label
defp payment_filter_label(nil), do: gettext("All")
defp payment_filter_label(:paid), do: gettext("Paid")
@@ -550,39 +406,6 @@ defmodule MvWeb.Components.MemberFilterComponent do
end
end
- # Get CSS classes for per-group filter label based on current state
- defp group_filter_label_class(group_filters, group_id, expected_value) do
- base_classes = "join-item btn btn-sm"
- current_value = Map.get(group_filters, to_string(group_id))
- is_active = current_value == expected_value
-
- cond do
- expected_value == nil ->
- if is_active do
- "#{base_classes} btn-active"
- else
- "#{base_classes} btn"
- end
-
- expected_value == :in ->
- if is_active do
- "#{base_classes} btn-success btn-active"
- else
- "#{base_classes} btn"
- end
-
- expected_value == :not_in ->
- if is_active do
- "#{base_classes} btn-error btn-active"
- else
- "#{base_classes} btn"
- end
-
- true ->
- "#{base_classes} btn-outline"
- end
- end
-
# Get CSS classes for boolean filter label based on current state
defp boolean_filter_label_class(boolean_filters, custom_field_id, expected_value) do
base_classes = "join-item btn btn-sm"
diff --git a/lib/mv_web/live/member_live/index.ex b/lib/mv_web/live/member_live/index.ex
index 59ee8f9..881be53 100644
--- a/lib/mv_web/live/member_live/index.ex
+++ b/lib/mv_web/live/member_live/index.ex
@@ -41,7 +41,6 @@ defmodule MvWeb.MemberLive.Index do
@custom_field_prefix Mv.Constants.custom_field_prefix()
@boolean_filter_prefix Mv.Constants.boolean_filter_prefix()
- @group_filter_prefix "group_"
# Maximum number of boolean custom field filters allowed per request (DoS protection)
@max_boolean_filters Mv.Constants.max_boolean_filters()
@@ -86,12 +85,6 @@ defmodule MvWeb.MemberLive.Index do
|> Enum.filter(&(&1.value_type == :boolean))
|> Enum.sort_by(& &1.name, :asc)
- # Load groups for filter dropdown (sorted by name)
- groups =
- Mv.Membership.Group
- |> Ash.Query.sort(name: :asc)
- |> Ash.read!(actor: actor)
-
# Load settings once to avoid N+1 queries
settings =
case Membership.get_settings() do
@@ -122,8 +115,6 @@ defmodule MvWeb.MemberLive.Index do
|> assign_new(:sort_field, fn -> :first_name end)
|> assign_new(:sort_order, fn -> :asc end)
|> assign(:cycle_status_filter, nil)
- |> assign(:group_filters, %{})
- |> assign(:groups, groups)
|> assign(:boolean_custom_field_filters, %{})
|> assign(:selected_members, MapSet.new())
|> assign(:settings, settings)
@@ -251,7 +242,6 @@ defmodule MvWeb.MemberLive.Index do
socket.assigns.sort_field,
socket.assigns.sort_order,
socket.assigns.cycle_status_filter,
- socket.assigns[:group_filters],
new_show_current,
socket.assigns.boolean_custom_field_filters
)
@@ -362,7 +352,6 @@ defmodule MvWeb.MemberLive.Index do
export_sort_field(socket.assigns.sort_field),
export_sort_order(socket.assigns.sort_order),
socket.assigns.cycle_status_filter,
- socket.assigns[:group_filters],
socket.assigns.show_current_cycle,
socket.assigns.boolean_custom_field_filters
)
@@ -388,7 +377,6 @@ defmodule MvWeb.MemberLive.Index do
socket.assigns.sort_field,
socket.assigns.sort_order,
socket.assigns.cycle_status_filter,
- socket.assigns[:group_filters],
socket.assigns.show_current_cycle,
socket.assigns.boolean_custom_field_filters
)
@@ -416,7 +404,6 @@ defmodule MvWeb.MemberLive.Index do
socket.assigns.sort_field,
socket.assigns.sort_order,
filter,
- socket.assigns[:group_filters],
socket.assigns.show_current_cycle,
socket.assigns.boolean_custom_field_filters
)
@@ -450,7 +437,6 @@ defmodule MvWeb.MemberLive.Index do
socket.assigns.sort_field,
socket.assigns.sort_order,
socket.assigns.cycle_status_filter,
- socket.assigns[:group_filters],
socket.assigns.show_current_cycle,
updated_filters
)
@@ -463,55 +449,11 @@ defmodule MvWeb.MemberLive.Index do
{:noreply, push_patch(socket, to: new_path, replace: true)}
end
- @impl true
- def handle_info({:group_filter_changed, group_id_str, filter_value}, socket) do
- normalized_id = normalize_uuid_string(group_id_str) || group_id_str
-
- group_filters =
- if filter_value == nil do
- Map.delete(socket.assigns.group_filters, normalized_id)
- else
- Map.put(socket.assigns.group_filters, normalized_id, filter_value)
- end
-
- socket =
- socket
- |> assign(:group_filters, group_filters)
- |> load_members()
- |> update_selection_assigns()
-
- query_params =
- build_query_params(
- socket.assigns.query,
- socket.assigns.sort_field,
- socket.assigns.sort_order,
- socket.assigns.cycle_status_filter,
- group_filters,
- socket.assigns.show_current_cycle,
- socket.assigns.boolean_custom_field_filters
- )
- |> maybe_add_field_selection(
- socket.assigns[:user_field_selection],
- socket.assigns[:fields_in_url?] || false
- )
-
- new_path = ~p"/members?#{query_params}"
- {:noreply, push_patch(socket, to: new_path, replace: true)}
- end
-
@impl true
def handle_info({:reset_all_filters, cycle_status_filter, boolean_filters}, socket) do
- handle_info({:reset_all_filters, cycle_status_filter, boolean_filters, %{}}, socket)
- end
-
- def handle_info(
- {:reset_all_filters, cycle_status_filter, boolean_filters, group_filters},
- socket
- ) do
socket =
socket
|> assign(:cycle_status_filter, cycle_status_filter)
- |> assign(:group_filters, group_filters)
|> assign(:boolean_custom_field_filters, boolean_filters)
|> load_members()
|> update_selection_assigns()
@@ -522,7 +464,6 @@ defmodule MvWeb.MemberLive.Index do
socket.assigns.sort_field,
socket.assigns.sort_order,
cycle_status_filter,
- socket.assigns[:group_filters],
socket.assigns.show_current_cycle,
boolean_filters
)
@@ -659,7 +600,6 @@ defmodule MvWeb.MemberLive.Index do
|> maybe_update_search(params)
|> maybe_update_sort(params)
|> maybe_update_cycle_status_filter(params)
- |> maybe_update_group_filters(params)
|> maybe_update_boolean_filters(params)
|> maybe_update_show_current_cycle(params)
|> assign(:fields_in_url?, fields_in_url?)
@@ -693,7 +633,6 @@ defmodule MvWeb.MemberLive.Index do
socket.assigns.sort_field,
socket.assigns.sort_order,
socket.assigns.cycle_status_filter,
- socket.assigns[:group_filters],
socket.assigns.show_current_cycle,
socket.assigns.boolean_custom_field_filters,
socket.assigns.user_field_selection,
@@ -787,7 +726,6 @@ defmodule MvWeb.MemberLive.Index do
socket.assigns.sort_field,
socket.assigns.sort_order,
socket.assigns.cycle_status_filter,
- socket.assigns[:group_filters],
socket.assigns.show_current_cycle,
socket.assigns.boolean_custom_field_filters
)
@@ -806,63 +744,50 @@ defmodule MvWeb.MemberLive.Index do
sort_field,
sort_order,
cycle_status_filter,
- group_filters,
show_current_cycle,
boolean_filters
) do
- base_params = build_base_params(query, sort_field, sort_order)
- base_params = add_cycle_status_filter(base_params, cycle_status_filter)
- base_params = add_group_filters(base_params, group_filters)
- base_params = add_show_current_cycle(base_params, show_current_cycle)
- add_boolean_filters(base_params, boolean_filters)
- end
+ field_str =
+ if is_atom(sort_field) do
+ Atom.to_string(sort_field)
+ else
+ sort_field
+ end
- defp build_base_params(query, sort_field, sort_order) do
- %{
- "query" => query || "",
- "sort_field" => normalize_sort_field(sort_field),
- "sort_order" => normalize_sort_order(sort_order)
+ order_str =
+ if is_atom(sort_order) do
+ Atom.to_string(sort_order)
+ else
+ sort_order
+ end
+
+ base_params = %{
+ "query" => query,
+ "sort_field" => field_str,
+ "sort_order" => order_str
}
- end
- defp normalize_sort_field(nil), do: ""
- defp normalize_sort_field(field) when is_atom(field), do: Atom.to_string(field)
- defp normalize_sort_field(field) when is_binary(field), do: field
- defp normalize_sort_field(_), do: ""
+ base_params =
+ case cycle_status_filter do
+ nil -> base_params
+ :paid -> Map.put(base_params, "cycle_status_filter", "paid")
+ :unpaid -> Map.put(base_params, "cycle_status_filter", "unpaid")
+ end
- defp normalize_sort_order(nil), do: ""
- defp normalize_sort_order(order) when is_atom(order), do: Atom.to_string(order)
- defp normalize_sort_order(order) when is_binary(order), do: order
- defp normalize_sort_order(_), do: ""
+ base_params =
+ if show_current_cycle do
+ Map.put(base_params, "show_current_cycle", "true")
+ else
+ base_params
+ end
- defp add_group_filters(params, group_filters) do
- Enum.reduce(group_filters, params, fn {group_id_str, value}, acc ->
- param_value = if value == :in, do: "in", else: "not_in"
- Map.put(acc, "#{@group_filter_prefix}#{group_id_str}", param_value)
+ Enum.reduce(boolean_filters, base_params, fn {custom_field_id, filter_value}, acc ->
+ param_key = "#{@boolean_filter_prefix}#{custom_field_id}"
+ param_value = if filter_value == true, do: "true", else: "false"
+ Map.put(acc, param_key, param_value)
end)
end
- defp add_cycle_status_filter(params, nil), do: params
- defp add_cycle_status_filter(params, :paid), do: Map.put(params, "cycle_status_filter", "paid")
-
- defp add_cycle_status_filter(params, :unpaid),
- do: Map.put(params, "cycle_status_filter", "unpaid")
-
- defp add_cycle_status_filter(params, _), do: params
-
- defp add_show_current_cycle(params, true), do: Map.put(params, "show_current_cycle", "true")
- defp add_show_current_cycle(params, _), do: params
-
- defp add_boolean_filters(params, boolean_filters) do
- Enum.reduce(boolean_filters, params, &add_boolean_filter/2)
- end
-
- defp add_boolean_filter({custom_field_id, filter_value}, acc) do
- param_key = "#{@boolean_filter_prefix}#{custom_field_id}"
- param_value = if filter_value == true, do: "true", else: "false"
- Map.put(acc, param_key, param_value)
- end
-
# -------------------------------------------------------------
# Loading members
# -------------------------------------------------------------
@@ -898,14 +823,8 @@ defmodule MvWeb.MemberLive.Index do
query = MembershipFeeStatus.load_cycles_for_members(query, socket.assigns.show_current_cycle)
- # Load groups for each member (id, name, slug only)
- query =
- Ash.Query.load(query, groups: [:id, :name, :slug])
-
query = apply_search_filter(query, search_query)
- query = apply_group_filters(query, socket.assigns[:group_filters], socket.assigns[:groups])
-
# Use ALL custom fields for sorting (not just show_in_overview subset)
custom_fields_for_sort = socket.assigns.all_custom_fields
@@ -941,7 +860,7 @@ defmodule MvWeb.MemberLive.Index do
socket.assigns.all_custom_fields
)
- # Sort in memory if needed (custom fields, groups, group_count; computed fields are blocked)
+ # Sort in memory if needed (custom fields only; computed fields are blocked)
members =
if sort_after_load and
socket.assigns.sort_field not in FieldVisibility.computed_member_fields() do
@@ -983,51 +902,6 @@ defmodule MvWeb.MemberLive.Index do
end
end
- # Multiple group filters combine with AND: member must match all selected group conditions.
- defp apply_group_filters(query, group_filters, _groups) when group_filters == %{}, do: query
-
- defp apply_group_filters(query, group_filters, groups) do
- valid_ids =
- groups
- |> Enum.map(&normalize_uuid_string(to_string(&1.id)))
- |> Enum.reject(&is_nil/1)
- |> MapSet.new()
-
- Enum.reduce(group_filters, query, fn {group_id_str, value}, q ->
- member? = MapSet.member?(valid_ids, group_id_str)
-
- if member? do
- apply_one_group_filter(q, group_id_str, value)
- else
- q
- end
- end)
- end
-
- defp apply_one_group_filter(query, _group_id_str, nil), do: query
-
- defp apply_one_group_filter(query, group_id_str, :in) do
- case Ecto.UUID.cast(group_id_str) do
- {:ok, group_uuid} ->
- Ash.Query.filter(query, expr(exists(member_groups, group_id == ^group_uuid)))
-
- _ ->
- query
- end
- end
-
- defp apply_one_group_filter(query, group_id_str, :not_in) do
- case Ecto.UUID.cast(group_id_str) do
- {:ok, group_uuid} ->
- Ash.Query.filter(query, expr(not exists(member_groups, group_id == ^group_uuid)))
-
- _ ->
- query
- end
- end
-
- defp apply_one_group_filter(query, _, _), do: query
-
defp apply_cycle_status_filter(members, nil, _show_current), do: members
defp apply_cycle_status_filter(members, status, show_current)
@@ -1063,10 +937,6 @@ defmodule MvWeb.MemberLive.Index do
defp apply_sort_to_query(query, field, order) do
cond do
- # Groups sort -> after load (in memory)
- field in [:groups, "groups"] ->
- {query, true}
-
# Custom field sort -> after load
custom_field_sort?(field) ->
{query, true}
@@ -1106,14 +976,12 @@ defmodule MvWeb.MemberLive.Index do
defp valid_sort_field_db_or_custom?(field) when is_atom(field) do
non_sortable_fields = [:notes]
valid_fields = Mv.Constants.member_fields() -- non_sortable_fields
- field in valid_fields or custom_field_sort?(field) or field == :groups
+ field in valid_fields or custom_field_sort?(field)
end
defp valid_sort_field_db_or_custom?(field) when is_binary(field) do
- normalized = if field == "groups", do: :groups, else: safe_member_field_atom_only(field)
-
- (normalized != nil and valid_sort_field_db_or_custom?(normalized)) or
- custom_field_sort?(field)
+ custom_field_sort?(field) or
+ ((atom = safe_member_field_atom_only(field)) != nil and valid_sort_field_db_or_custom?(atom))
end
defp safe_member_field_atom_only(str) do
@@ -1156,35 +1024,14 @@ defmodule MvWeb.MemberLive.Index do
end
defp sort_members_in_memory(members, field, order, custom_fields) do
- if field in [:groups, "groups"] do
- sort_members_by_groups(members, order)
- else
- custom_field_id_str = extract_custom_field_id(field)
+ custom_field_id_str = extract_custom_field_id(field)
- case custom_field_id_str do
- nil -> members
- id_str -> sort_members_by_custom_field(members, id_str, order, custom_fields)
- end
+ case custom_field_id_str do
+ nil -> members
+ id_str -> sort_members_by_custom_field(members, id_str, order, custom_fields)
end
end
- defp sort_members_by_groups(members, order) do
- # Members with groups first, then by first group name alphabetically (min = first by sort order)
- first_group_name = fn member ->
- (member.groups || [])
- |> Enum.map(& &1.name)
- |> Enum.min(fn -> nil end)
- end
-
- members
- |> Enum.sort_by(fn member ->
- name = first_group_name.(member)
- # Nil (no groups) sorts last in asc, first in desc
- {name == nil, name || ""}
- end)
- |> then(fn list -> if order == :desc, do: Enum.reverse(list), else: list end)
- end
-
defp sort_members_by_custom_field(members, id_str, order, custom_fields) do
custom_field = find_custom_field_by_id(custom_fields, id_str)
@@ -1279,16 +1126,11 @@ defmodule MvWeb.MemberLive.Index do
defp determine_field(default, _), do: default
defp determine_field_after_computed_check(default, sf) when is_binary(sf) do
- cond do
- sf == "groups" ->
- :groups
-
- custom_field_sort?(sf) ->
- if valid_sort_field?(sf), do: sf, else: default
-
- true ->
- atom = safe_member_field_atom_only(sf)
- if atom != nil and valid_sort_field?(atom), do: atom, else: default
+ if custom_field_sort?(sf) do
+ if valid_sort_field?(sf), do: sf, else: default
+ else
+ atom = safe_member_field_atom_only(sf)
+ if atom != nil and valid_sort_field?(atom), do: atom, else: default
end
end
@@ -1318,62 +1160,6 @@ defmodule MvWeb.MemberLive.Index do
defp maybe_update_cycle_status_filter(socket, _params),
do: assign(socket, :cycle_status_filter, nil)
- defp maybe_update_group_filters(socket, params) when is_map(params) do
- prefix = @group_filter_prefix
- prefix_len = String.length(prefix)
-
- group_param_entries =
- params
- |> Enum.filter(fn {key, _} ->
- key_str = to_string(key)
- String.starts_with?(key_str, prefix)
- end)
-
- filters =
- Enum.reduce(group_param_entries, %{}, fn {key, value_str}, acc ->
- add_group_filter_entry(acc, key, value_str, prefix_len)
- end)
-
- assign(socket, :group_filters, filters)
- end
-
- defp maybe_update_group_filters(socket, _), do: socket
-
- defp add_group_filter_entry(acc, key, value_str, prefix_len) do
- key_str = to_string(key)
- raw_id = String.slice(key_str, prefix_len, String.length(key_str) - prefix_len)
- group_id_str = normalize_uuid_string(raw_id)
- valid_id? = group_id_str && String.length(group_id_str) <= @max_uuid_length
-
- if valid_id? do
- case parse_group_filter_value(value_str) do
- nil -> acc
- value -> Map.put(acc, group_id_str, value)
- end
- else
- acc
- end
- end
-
- # Normalize UUID string so URL params match valid_ids (lowercase, canonical format)
- defp normalize_uuid_string(raw) when is_binary(raw) do
- case Ecto.UUID.cast(String.trim(raw)) do
- {:ok, uuid} -> to_string(uuid)
- _ -> raw
- end
- end
-
- defp normalize_uuid_string(_), do: nil
-
- defp parse_group_filter_value("in"), do: :in
- defp parse_group_filter_value("not_in"), do: :not_in
-
- defp parse_group_filter_value(val) when is_binary(val) do
- parse_group_filter_value(String.trim(val))
- end
-
- defp parse_group_filter_value(_), do: nil
-
defp determine_cycle_status_filter("paid"), do: :paid
defp determine_cycle_status_filter("unpaid"), do: :unpaid
defp determine_cycle_status_filter(_), do: nil
diff --git a/lib/mv_web/live/member_live/index.html.heex b/lib/mv_web/live/member_live/index.html.heex
index 311447b..381cd63 100644
--- a/lib/mv_web/live/member_live/index.html.heex
+++ b/lib/mv_web/live/member_live/index.html.heex
@@ -2,12 +2,20 @@
<.header>
{gettext("Members")}
<:actions>
- <.live_component
- module={MvWeb.Components.ExportDropdown}
- id="export-dropdown"
- export_payload_json={@export_payload_json}
- selected_count={@selected_count}
- />
+
<.button
class="secondary"
id="copy-emails-btn"
@@ -48,8 +56,6 @@
module={MvWeb.Components.MemberFilterComponent}
id="member-filter"
cycle_status_filter={@cycle_status_filter}
- groups={@groups}
- group_filters={@group_filters}
boolean_custom_fields={@boolean_custom_fields}
boolean_filters={@boolean_custom_field_filters}
member_count={length(@members)}
@@ -304,34 +310,6 @@
{gettext("No cycle")}
<% end %>
- <:col
- :let={member}
- label={
- ~H"""
- <.live_component
- module={MvWeb.Components.SortHeaderComponent}
- id={:sort_groups}
- field={:groups}
- label={gettext("Groups")}
- sort_field={@sort_field}
- sort_order={@sort_order}
- />
- """
- }
- >
- <%= for group <- (member.groups || []) do %>
-
- {group.name}
-
- <% end %>
- <%= if (member.groups || []) == [] do %>
- —
- <% end %>
-
<:action :let={member}>
<.link navigate={~p"/members/#{member}"}>{gettext("Show")}
diff --git a/lib/mv_web/router.ex b/lib/mv_web/router.ex
index 61532ff..4f8c8a5 100644
--- a/lib/mv_web/router.ex
+++ b/lib/mv_web/router.ex
@@ -95,7 +95,6 @@ defmodule MvWeb.Router do
live "/admin/import-export", ImportExportLive
post "/members/export.csv", MemberExportController, :export
- post "/members/export.pdf", MemberPdfExportController, :export
post "/set_locale", LocaleController, :set_locale
end
diff --git a/mix.exs b/mix.exs
index 6ac9e8d..8ca214c 100644
--- a/mix.exs
+++ b/mix.exs
@@ -79,8 +79,7 @@ defmodule Mv.MixProject do
{:picosat_elixir, "~> 0.1"},
{:ecto_commons, "~> 0.3"},
{:slugify, "~> 1.3"},
- {:nimble_csv, "~> 1.0"},
- {:imprintor, "~> 0.5.0"}
+ {:nimble_csv, "~> 1.0"}
]
end
diff --git a/mix.lock b/mix.lock
index 0b581ff..f698fa5 100644
--- a/mix.lock
+++ b/mix.lock
@@ -36,7 +36,6 @@
"hpax": {:hex, :hpax, "1.0.3", "ed67ef51ad4df91e75cc6a1494f851850c0bd98ebc0be6e81b026e765ee535aa", [:mix], [], "hexpm", "8eab6e1cfa8d5918c2ce4ba43588e894af35dbd8e91e6e55c817bca5847df34a"},
"idna": {:hex, :idna, "6.1.1", "8a63070e9f7d0c62eb9d9fcb360a7de382448200fbbd1b106cc96d3d8099df8d", [:rebar3], [{:unicode_util_compat, "~> 0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "92376eb7894412ed19ac475e4a86f7b413c1b9fbb5bd16dccd57934157944cea"},
"igniter": {:hex, :igniter, "0.7.2", "81c132c0df95963c7a228f74a32d3348773743ed9651f24183bfce0fe6ff16d1", [:mix], [{:glob_ex, "~> 0.1.7", [hex: :glob_ex, repo: "hexpm", optional: false]}, {:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}, {:owl, "~> 0.11", [hex: :owl, repo: "hexpm", optional: false]}, {:phx_new, "~> 1.7", [hex: :phx_new, repo: "hexpm", optional: true]}, {:req, "~> 0.5", [hex: :req, repo: "hexpm", optional: false]}, {:rewrite, ">= 1.1.1 and < 2.0.0-0", [hex: :rewrite, repo: "hexpm", optional: false]}, {:sourceror, "~> 1.4", [hex: :sourceror, repo: "hexpm", optional: false]}, {:spitfire, ">= 0.1.3 and < 1.0.0-0", [hex: :spitfire, repo: "hexpm", optional: false]}], "hexpm", "f4cab73ec31f4fb452de1a17037f8a08826105265aa2d76486fcb848189bef9b"},
- "imprintor": {:hex, :imprintor, "0.5.0", "3266aa8487cc6eab3915a578c79d49e489d1bacf959a6535b1ef32acc62d71cc", [:mix], [{:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}, {:rustler, ">= 0.0.0", [hex: :rustler, repo: "hexpm", optional: true]}, {:rustler_precompiled, "~> 0.8", [hex: :rustler_precompiled, repo: "hexpm", optional: false]}], "hexpm", "d4bbfbd26c2ddbb7eb38894b7412c0ef62f953cbb176df3cccbd266fe890c12f"},
"iterex": {:hex, :iterex, "0.1.2", "58f9b9b9a22a55cbfc7b5234a9c9c63eaac26d276b3db80936c0e1c60355a5a6", [:mix], [], "hexpm", "2e103b8bcc81757a9af121f6dc0df312c9a17220f302b1193ef720460d03029d"},
"jason": {:hex, :jason, "1.4.4", "b9226785a9aa77b6857ca22832cffa5d5011a667207eb2a0ad56adb5db443b8a", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "c5eb0cab91f094599f94d55bc63409236a8ec69a21a67814529e8d5f6cc90b3b"},
"joken": {:hex, :joken, "2.6.2", "5daaf82259ca603af4f0b065475099ada1b2b849ff140ccd37f4b6828ca6892a", [:mix], [{:jose, "~> 1.11.10", [hex: :jose, repo: "hexpm", optional: false]}], "hexpm", "5134b5b0a6e37494e46dbf9e4dad53808e5e787904b7c73972651b51cce3d72b"},
@@ -69,12 +68,11 @@
"reactor": {:hex, :reactor, "1.0.0", "024bd13df910bcb8c01cebed4f10bd778269a141a1c8a234e4f67796ac4883cf", [:mix], [{:igniter, "~> 0.4", [hex: :igniter, repo: "hexpm", optional: true]}, {:iterex, "~> 0.1", [hex: :iterex, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:libgraph, "~> 0.16", [hex: :libgraph, repo: "hexpm", optional: false]}, {:spark, ">= 2.3.3 and < 3.0.0-0", [hex: :spark, repo: "hexpm", optional: false]}, {:splode, "~> 0.2", [hex: :splode, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.2", [hex: :telemetry, repo: "hexpm", optional: false]}, {:yaml_elixir, "~> 2.11", [hex: :yaml_elixir, repo: "hexpm", optional: false]}, {:ymlr, "~> 5.0", [hex: :ymlr, repo: "hexpm", optional: false]}], "hexpm", "ae8eb507fffc517f5aa5947db9d2ede2db8bae63b66c94ccb5a2027d30f830a0"},
"req": {:hex, :req, "0.5.17", "0096ddd5b0ed6f576a03dde4b158a0c727215b15d2795e59e0916c6971066ede", [:mix], [{:brotli, "~> 0.3.1", [hex: :brotli, repo: "hexpm", optional: true]}, {:ezstd, "~> 1.0", [hex: :ezstd, repo: "hexpm", optional: true]}, {:finch, "~> 0.17", [hex: :finch, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mime, "~> 2.0.6 or ~> 2.1", [hex: :mime, repo: "hexpm", optional: false]}, {:nimble_csv, "~> 1.0", [hex: :nimble_csv, repo: "hexpm", optional: true]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "0b8bc6ffdfebbc07968e59d3ff96d52f2202d0536f10fef4dc11dc02a2a43e39"},
"rewrite": {:hex, :rewrite, "1.2.0", "80220eb14010e175b67c939397e1a8cdaa2c32db6e2e0a9d5e23e45c0414ce21", [:mix], [{:glob_ex, "~> 0.1", [hex: :glob_ex, repo: "hexpm", optional: false]}, {:sourceror, "~> 1.0", [hex: :sourceror, repo: "hexpm", optional: false]}, {:text_diff, "~> 0.1", [hex: :text_diff, repo: "hexpm", optional: false]}], "hexpm", "a1cd702bbb9d51613ab21091f04a386d750fc6f4516b81900df082d78b2d8c50"},
- "rustler_precompiled": {:hex, :rustler_precompiled, "0.8.4", "700a878312acfac79fb6c572bb8b57f5aae05fe1cf70d34b5974850bbf2c05bf", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: false]}, {:rustler, "~> 0.23", [hex: :rustler, repo: "hexpm", optional: true]}], "hexpm", "3b33d99b540b15f142ba47944f7a163a25069f6d608783c321029bc1ffb09514"},
"slugify": {:hex, :slugify, "1.3.1", "0d3b8b7e5c1eeaa960e44dce94382bee34a39b3ea239293e457a9c5b47cc6fd3", [:mix], [], "hexpm", "cb090bbeb056b312da3125e681d98933a360a70d327820e4b7f91645c4d8be76"},
"sobelow": {:hex, :sobelow, "0.14.1", "2f81e8632f15574cba2402bcddff5497b413c01e6f094bc0ab94e83c2f74db81", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "8fac9a2bd90fdc4b15d6fca6e1608efb7f7c600fa75800813b794ee9364c87f2"},
"sourceror": {:hex, :sourceror, "1.10.1", "325753ed460fe9fa34ebb4deda76d57b2e1507dcd78a5eb9e1c41bfb78b7cdfe", [:mix], [], "hexpm", "288f3079d93865cd1e3e20df5b884ef2cb440e0e03e8ae393624ee8a770ba588"},
"spark": {:hex, :spark, "2.4.0", "f93d3ae6b5f3004e956d52f359fa40670366685447631bc7c058f4fbf250ebf3", [:mix], [{:igniter, ">= 0.3.64 and < 1.0.0-0", [hex: :igniter, repo: "hexpm", optional: true]}, {:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: true]}, {:sourceror, "~> 1.2", [hex: :sourceror, repo: "hexpm", optional: true]}], "hexpm", "4e5185f5737cd987bb9ef377ae3462a55b8312f5007c2bc4ad6e850d14ac0111"},
- "spitfire": {:hex, :spitfire, "0.3.3", "be195b27648f21454932bf46014455cdbce4fca55fef1f0e41d36076c47b6c4a", [:mix], [], "hexpm", "5dc51c3b61a1d98cdcac1c130f0a374d22d51beed982df90834bdd616356e1fa"},
+ "spitfire": {:hex, :spitfire, "0.3.3", "be195b27648f21454932bf46014455cdbce4fca55fef1f0e41d36076c47b6c4a", [], [], "hexpm", "5dc51c3b61a1d98cdcac1c130f0a374d22d51beed982df90834bdd616356e1fa"},
"splode": {:hex, :splode, "0.3.0", "ff8effecc509a51245df2f864ec78d849248647c37a75886033e3b1a53ca9470", [:mix], [], "hexpm", "73cfd0892d7316d6f2c93e6e8784bd6e137b2aa38443de52fd0a25171d106d81"},
"stream_data": {:hex, :stream_data, "1.2.0", "58dd3f9e88afe27dc38bef26fce0c84a9e7a96772b2925c7b32cd2435697a52b", [:mix], [], "hexpm", "eb5c546ee3466920314643edf68943a5b14b32d1da9fe01698dc92b73f89a9ed"},
"sweet_xml": {:hex, :sweet_xml, "0.7.5", "803a563113981aaac202a1dbd39771562d0ad31004ddbfc9b5090bdcd5605277", [:mix], [], "hexpm", "193b28a9b12891cae351d81a0cead165ffe67df1b73fe5866d10629f4faefb12"},
diff --git a/priv/fonts/.gitkeep b/priv/fonts/.gitkeep
deleted file mode 100644
index 0baaaae..0000000
--- a/priv/fonts/.gitkeep
+++ /dev/null
@@ -1,5 +0,0 @@
-# This file ensures the fonts directory is tracked by git
-# Place TTF font files here
-
-
-
diff --git a/priv/fonts/LiberationMono-Bold.ttf b/priv/fonts/LiberationMono-Bold.ttf
deleted file mode 100644
index 2e46737..0000000
Binary files a/priv/fonts/LiberationMono-Bold.ttf and /dev/null differ
diff --git a/priv/fonts/LiberationMono-BoldItalic.ttf b/priv/fonts/LiberationMono-BoldItalic.ttf
deleted file mode 100644
index d1f46d7..0000000
Binary files a/priv/fonts/LiberationMono-BoldItalic.ttf and /dev/null differ
diff --git a/priv/fonts/LiberationMono-Italic.ttf b/priv/fonts/LiberationMono-Italic.ttf
deleted file mode 100644
index 954c394..0000000
Binary files a/priv/fonts/LiberationMono-Italic.ttf and /dev/null differ
diff --git a/priv/fonts/LiberationMono-Regular.ttf b/priv/fonts/LiberationMono-Regular.ttf
deleted file mode 100644
index e774859..0000000
Binary files a/priv/fonts/LiberationMono-Regular.ttf and /dev/null differ
diff --git a/priv/fonts/LiberationSans-Bold.ttf b/priv/fonts/LiberationSans-Bold.ttf
deleted file mode 100644
index dc5d57f..0000000
Binary files a/priv/fonts/LiberationSans-Bold.ttf and /dev/null differ
diff --git a/priv/fonts/LiberationSans-BoldItalic.ttf b/priv/fonts/LiberationSans-BoldItalic.ttf
deleted file mode 100644
index 158488a..0000000
Binary files a/priv/fonts/LiberationSans-BoldItalic.ttf and /dev/null differ
diff --git a/priv/fonts/LiberationSans-Italic.ttf b/priv/fonts/LiberationSans-Italic.ttf
deleted file mode 100644
index 25970d9..0000000
Binary files a/priv/fonts/LiberationSans-Italic.ttf and /dev/null differ
diff --git a/priv/fonts/LiberationSans-Regular.ttf b/priv/fonts/LiberationSans-Regular.ttf
deleted file mode 100644
index e633985..0000000
Binary files a/priv/fonts/LiberationSans-Regular.ttf and /dev/null differ
diff --git a/priv/fonts/LiberationSerif-Bold.ttf b/priv/fonts/LiberationSerif-Bold.ttf
deleted file mode 100644
index 3c7c55b..0000000
Binary files a/priv/fonts/LiberationSerif-Bold.ttf and /dev/null differ
diff --git a/priv/fonts/LiberationSerif-BoldItalic.ttf b/priv/fonts/LiberationSerif-BoldItalic.ttf
deleted file mode 100644
index 6b35d9f..0000000
Binary files a/priv/fonts/LiberationSerif-BoldItalic.ttf and /dev/null differ
diff --git a/priv/fonts/LiberationSerif-Italic.ttf b/priv/fonts/LiberationSerif-Italic.ttf
deleted file mode 100644
index 54d5164..0000000
Binary files a/priv/fonts/LiberationSerif-Italic.ttf and /dev/null differ
diff --git a/priv/fonts/LiberationSerif-Regular.ttf b/priv/fonts/LiberationSerif-Regular.ttf
deleted file mode 100644
index 5e5550c..0000000
Binary files a/priv/fonts/LiberationSerif-Regular.ttf and /dev/null differ
diff --git a/priv/gettext/de/LC_MESSAGES/default.po b/priv/gettext/de/LC_MESSAGES/default.po
index 1784d4b..d8f80c1 100644
--- a/priv/gettext/de/LC_MESSAGES/default.po
+++ b/priv/gettext/de/LC_MESSAGES/default.po
@@ -150,7 +150,6 @@ msgstr "Hausnummer"
msgid "Notes"
msgstr "Notizen"
-#: lib/mv/membership/members_pdf.ex
#: lib/mv_web/live/components/member_filter_component.ex
#: lib/mv_web/live/member_live/show.ex
#: lib/mv_web/live/member_live/show/membership_fees_component.ex
@@ -933,7 +932,6 @@ msgstr "Vierteljährlich"
msgid "Status"
msgstr "Status"
-#: lib/mv/membership/members_pdf.ex
#: lib/mv_web/live/member_live/show.ex
#: lib/mv_web/live/member_live/show/membership_fees_component.ex
#: lib/mv_web/live/statistics_live.ex
@@ -942,7 +940,6 @@ msgstr "Status"
msgid "Suspended"
msgstr "Pausiert"
-#: lib/mv/membership/members_pdf.ex
#: lib/mv_web/live/components/member_filter_component.ex
#: lib/mv_web/live/member_live/show.ex
#: lib/mv_web/live/member_live/show/membership_fees_component.ex
@@ -2199,9 +2196,7 @@ msgid "Group saved successfully."
msgstr "Gruppe erfolgreich gespeichert."
#: lib/mv_web/components/layouts/sidebar.ex
-#: lib/mv_web/live/components/member_filter_component.ex
#: lib/mv_web/live/group_live/index.ex
-#: lib/mv_web/live/member_live/index.html.heex
#, elixir-autogen, elixir-format
msgid "Groups"
msgstr "Gruppen"
@@ -2396,12 +2391,17 @@ msgstr "Mitgliederdaten verwalten"
msgid "Use the data field name as the CSV column header in your file. Data fields must exist in Mila before importing, so they must be listed in the list of member data (like e-mail or first name). Unknown data field columns will be ignored with a warning."
msgstr "Verwende die Namen der Datenfelder als Spaltennamen in der CSV Datei. Datenfelder müssen in Mila bereits angelegt sein, damit sie importiert werden können. sie müssen in der Liste der Mitgliederdaten als Datenfeld enthalten sein (z.B. E-Mail). Spalten mit unbekannten Spaltenüberschriften werden mit einer Warnung ignoriert."
-#: lib/mv_web/components/export_dropdown.ex
+#: lib/mv_web/live/member_live/index.html.heex
#, elixir-autogen, elixir-format, fuzzy
msgid "Export members to CSV"
msgstr "Mitglieder importieren (CSV)"
-#: lib/mv_web/components/export_dropdown.ex
+#: lib/mv_web/live/member_live/index.html.heex
+#, elixir-autogen, elixir-format
+msgid "Export to CSV"
+msgstr "Nach CSV exportieren"
+
+#: lib/mv_web/live/member_live/index.html.heex
#, elixir-autogen, elixir-format
msgid "all"
msgstr "alle"
@@ -2473,11 +2473,6 @@ msgstr "Pausiert"
msgid "unpaid"
msgstr "Unbezahlt"
-#: lib/mv_web/live/member_live/index.html.heex
-#, elixir-autogen, elixir-format
-msgid "Member of group %{name}"
-msgstr "Mitglied der Gruppe %{name}"
-
#: lib/mv_web/live/statistics_live.ex
#, elixir-autogen, elixir-format
msgid "Active members"
@@ -2568,57 +2563,3 @@ msgstr "Mitgliederzahlen nach Jahr als Tabelle mit Balken"
#, elixir-autogen, elixir-format
msgid "Fee types could not be loaded."
msgstr "Beitragsarten konnten nicht geladen werden."
-
-#: lib/mv_web/components/export_dropdown.ex
-#, elixir-autogen, elixir-format
-msgid "CSV"
-msgstr "CSV"
-
-#: lib/mv/membership/members_pdf.ex
-#, elixir-autogen, elixir-format
-msgid "Created at:"
-msgstr "Erstellt am:"
-
-#: lib/mv_web/components/export_dropdown.ex
-#, elixir-autogen, elixir-format, fuzzy
-msgid "Export"
-msgstr "Nach CSV exportieren"
-
-#: lib/mv_web/controllers/member_pdf_export_controller.ex
-#, elixir-autogen, elixir-format
-msgid "Export contains %{count} rows, maximum is %{max}"
-msgstr "Export enthält %{count} Zeilen, Maximum ist %{max}"
-
-#: lib/mv_web/components/export_dropdown.ex
-#, elixir-autogen, elixir-format, fuzzy
-msgid "Export members to PDF"
-msgstr "Mitglieder als PDF exportieren"
-
-#: lib/mv_web/controllers/member_pdf_export_controller.ex
-#, elixir-autogen, elixir-format
-msgid "Failed to generate PDF export"
-msgstr "Erstellen des PDF Exports ist gescheitert"
-
-#: lib/mv/membership/members_pdf.ex
-#, elixir-autogen, elixir-format
-msgid "Member %{club_name}"
-msgstr "Mitglieder %{club_name}"
-
-#: lib/mv/membership/members_pdf.ex
-#, elixir-autogen, elixir-format
-msgid "Member count:"
-msgstr "Anzahl Mitglieder:"
-
-#: lib/mv_web/components/export_dropdown.ex
-#, elixir-autogen, elixir-format
-msgid "PDF"
-msgstr "PDF"
-
-#~ #: lib/mv_web/live/global_settings_live.ex
-#~ #, elixir-autogen, elixir-format, fuzzy
-#~ msgid "Custom Fields in CSV Import"
-#~ msgstr "Benutzerdefinierte Felder"
-
-#~ #, elixir-autogen, elixir-format
-#~ msgid "Failed to prepare CSV import: %{error}"
-#~ msgstr "Das Vorbereiten des CSV Imports ist gescheitert: %{error}"
diff --git a/priv/gettext/default.pot b/priv/gettext/default.pot
index af24afd..cf55012 100644
--- a/priv/gettext/default.pot
+++ b/priv/gettext/default.pot
@@ -151,7 +151,6 @@ msgstr ""
msgid "Notes"
msgstr ""
-#: lib/mv/membership/members_pdf.ex
#: lib/mv_web/live/components/member_filter_component.ex
#: lib/mv_web/live/member_live/show.ex
#: lib/mv_web/live/member_live/show/membership_fees_component.ex
@@ -934,7 +933,6 @@ msgstr ""
msgid "Status"
msgstr ""
-#: lib/mv/membership/members_pdf.ex
#: lib/mv_web/live/member_live/show.ex
#: lib/mv_web/live/member_live/show/membership_fees_component.ex
#: lib/mv_web/live/statistics_live.ex
@@ -943,7 +941,6 @@ msgstr ""
msgid "Suspended"
msgstr ""
-#: lib/mv/membership/members_pdf.ex
#: lib/mv_web/live/components/member_filter_component.ex
#: lib/mv_web/live/member_live/show.ex
#: lib/mv_web/live/member_live/show/membership_fees_component.ex
@@ -2200,9 +2197,7 @@ msgid "Group saved successfully."
msgstr ""
#: lib/mv_web/components/layouts/sidebar.ex
-#: lib/mv_web/live/components/member_filter_component.ex
#: lib/mv_web/live/group_live/index.ex
-#: lib/mv_web/live/member_live/index.html.heex
#, elixir-autogen, elixir-format
msgid "Groups"
msgstr ""
@@ -2397,12 +2392,17 @@ msgstr ""
msgid "Use the data field name as the CSV column header in your file. Data fields must exist in Mila before importing, so they must be listed in the list of member data (like e-mail or first name). Unknown data field columns will be ignored with a warning."
msgstr ""
-#: lib/mv_web/components/export_dropdown.ex
+#: lib/mv_web/live/member_live/index.html.heex
#, elixir-autogen, elixir-format
msgid "Export members to CSV"
msgstr ""
-#: lib/mv_web/components/export_dropdown.ex
+#: lib/mv_web/live/member_live/index.html.heex
+#, elixir-autogen, elixir-format
+msgid "Export to CSV"
+msgstr ""
+
+#: lib/mv_web/live/member_live/index.html.heex
#, elixir-autogen, elixir-format
msgid "all"
msgstr ""
@@ -2474,11 +2474,6 @@ msgstr ""
msgid "unpaid"
msgstr ""
-#: lib/mv_web/live/member_live/index.html.heex
-#, elixir-autogen, elixir-format
-msgid "Member of group %{name}"
-msgstr ""
-
#: lib/mv_web/live/statistics_live.ex
#, elixir-autogen, elixir-format
msgid "Active members"
@@ -2569,48 +2564,3 @@ msgstr ""
#, elixir-autogen, elixir-format
msgid "Fee types could not be loaded."
msgstr ""
-
-#: lib/mv_web/components/export_dropdown.ex
-#, elixir-autogen, elixir-format
-msgid "CSV"
-msgstr ""
-
-#: lib/mv/membership/members_pdf.ex
-#, elixir-autogen, elixir-format
-msgid "Created at:"
-msgstr ""
-
-#: lib/mv_web/components/export_dropdown.ex
-#, elixir-autogen, elixir-format
-msgid "Export"
-msgstr ""
-
-#: lib/mv_web/controllers/member_pdf_export_controller.ex
-#, elixir-autogen, elixir-format
-msgid "Export contains %{count} rows, maximum is %{max}"
-msgstr ""
-
-#: lib/mv_web/components/export_dropdown.ex
-#, elixir-autogen, elixir-format
-msgid "Export members to PDF"
-msgstr ""
-
-#: lib/mv_web/controllers/member_pdf_export_controller.ex
-#, elixir-autogen, elixir-format
-msgid "Failed to generate PDF export"
-msgstr ""
-
-#: lib/mv/membership/members_pdf.ex
-#, elixir-autogen, elixir-format
-msgid "Member %{club_name}"
-msgstr ""
-
-#: lib/mv/membership/members_pdf.ex
-#, elixir-autogen, elixir-format
-msgid "Member count:"
-msgstr ""
-
-#: lib/mv_web/components/export_dropdown.ex
-#, elixir-autogen, elixir-format
-msgid "PDF"
-msgstr ""
diff --git a/priv/gettext/en/LC_MESSAGES/default.po b/priv/gettext/en/LC_MESSAGES/default.po
index 88da6ff..1799738 100644
--- a/priv/gettext/en/LC_MESSAGES/default.po
+++ b/priv/gettext/en/LC_MESSAGES/default.po
@@ -13,7 +13,6 @@ msgstr ""
#: lib/mv_web/components/core_components.ex
#: lib/mv_web/live/group_live/index.ex
-#: lib/mv_web/live/group_live/show.ex
#, elixir-autogen, elixir-format
msgid "Actions"
msgstr ""
@@ -151,7 +150,6 @@ msgstr ""
msgid "Notes"
msgstr ""
-#: lib/mv/membership/members_pdf.ex
#: lib/mv_web/live/components/member_filter_component.ex
#: lib/mv_web/live/member_live/show.ex
#: lib/mv_web/live/member_live/show/membership_fees_component.ex
@@ -675,7 +673,6 @@ msgstr ""
msgid "A member with this email already exists. To link with a different member, please change one of the email addresses first."
msgstr ""
-#: lib/mv_web/live/group_live/show.ex
#: lib/mv_web/live/user_live/form.ex
#, elixir-autogen, elixir-format
msgid "Available members"
@@ -934,7 +931,6 @@ msgstr ""
msgid "Status"
msgstr ""
-#: lib/mv/membership/members_pdf.ex
#: lib/mv_web/live/member_live/show.ex
#: lib/mv_web/live/member_live/show/membership_fees_component.ex
#: lib/mv_web/live/statistics_live.ex
@@ -943,7 +939,6 @@ msgstr ""
msgid "Suspended"
msgstr ""
-#: lib/mv/membership/members_pdf.ex
#: lib/mv_web/live/components/member_filter_component.ex
#: lib/mv_web/live/member_live/show.ex
#: lib/mv_web/live/member_live/show/membership_fees_component.ex
@@ -2200,9 +2195,7 @@ msgid "Group saved successfully."
msgstr ""
#: lib/mv_web/components/layouts/sidebar.ex
-#: lib/mv_web/live/components/member_filter_component.ex
#: lib/mv_web/live/group_live/index.ex
-#: lib/mv_web/live/member_live/index.html.heex
#, elixir-autogen, elixir-format
msgid "Groups"
msgstr ""
@@ -2266,66 +2259,6 @@ msgstr ""
msgid "Could not load member search. Please try again."
msgstr ""
-#: lib/mv_web/live/group_live/show.ex
-#, elixir-autogen, elixir-format, fuzzy
-msgid "Add Member"
-msgstr ""
-
-#: lib/mv_web/live/group_live/show.ex
-#, elixir-autogen, elixir-format, fuzzy
-msgid "Failed to remove member: %{error}"
-msgstr ""
-
-#: lib/mv_web/live/group_live/show.ex
-#, elixir-autogen, elixir-format
-msgid "Member is not in this group."
-msgstr ""
-
-#: lib/mv_web/live/group_live/show.ex
-#, elixir-autogen, elixir-format, fuzzy
-msgid "No email"
-msgstr ""
-
-#: lib/mv_web/live/group_live/show.ex
-#, elixir-autogen, elixir-format
-msgid "Remove"
-msgstr ""
-
-#: lib/mv_web/live/group_live/show.ex
-#, elixir-autogen, elixir-format
-msgid "Remove member from group"
-msgstr ""
-
-#: lib/mv_web/live/group_live/show.ex
-#, elixir-autogen, elixir-format, fuzzy
-msgid "Search for a member"
-msgstr ""
-
-#: lib/mv_web/live/group_live/show.ex
-#, elixir-autogen, elixir-format, fuzzy
-msgid "Search for a member..."
-msgstr ""
-
-#: lib/mv_web/live/group_live/show.ex
-#, elixir-autogen, elixir-format
-msgid "Add members"
-msgstr ""
-
-#: lib/mv_web/live/group_live/show.ex
-#, elixir-autogen, elixir-format, fuzzy
-msgid "No members selected."
-msgstr ""
-
-#: lib/mv_web/live/group_live/show.ex
-#, elixir-autogen, elixir-format
-msgid "Remove %{name}"
-msgstr ""
-
-#: lib/mv_web/live/group_live/show.ex
-#, elixir-autogen, elixir-format
-msgid "Some members could not be added: %{errors}"
-msgstr ""
-
#: lib/mv_web/live/import_export_live/components.ex
#, elixir-autogen, elixir-format, fuzzy
msgid "CSV files only, maximum %{size} MB"
@@ -2397,12 +2330,17 @@ msgstr ""
msgid "Use the data field name as the CSV column header in your file. Data fields must exist in Mila before importing, so they must be listed in the list of member data (like e-mail or first name). Unknown data field columns will be ignored with a warning."
msgstr ""
-#: lib/mv_web/components/export_dropdown.ex
+#: lib/mv_web/live/member_live/index.html.heex
#, elixir-autogen, elixir-format, fuzzy
msgid "Export members to CSV"
msgstr ""
-#: lib/mv_web/components/export_dropdown.ex
+#: lib/mv_web/live/member_live/index.html.heex
+#, elixir-autogen, elixir-format
+msgid "Export to CSV"
+msgstr ""
+
+#: lib/mv_web/live/member_live/index.html.heex
#, elixir-autogen, elixir-format
msgid "all"
msgstr ""
@@ -2474,11 +2412,6 @@ msgstr ""
msgid "unpaid"
msgstr ""
-#: lib/mv_web/live/member_live/index.html.heex
-#, elixir-autogen, elixir-format
-msgid "Member of group %{name}"
-msgstr ""
-
#: lib/mv_web/live/statistics_live.ex
#, elixir-autogen, elixir-format, fuzzy
msgid "Active members"
@@ -2569,53 +2502,3 @@ msgstr ""
#, elixir-autogen, elixir-format
msgid "Fee types could not be loaded."
msgstr ""
-
-#: lib/mv_web/components/export_dropdown.ex
-#, elixir-autogen, elixir-format
-msgid "CSV"
-msgstr ""
-
-#: lib/mv/membership/members_pdf.ex
-#, elixir-autogen, elixir-format
-msgid "Created at:"
-msgstr "Created at:"
-
-#: lib/mv_web/components/export_dropdown.ex
-#, elixir-autogen, elixir-format, fuzzy
-msgid "Export"
-msgstr ""
-
-#: lib/mv_web/controllers/member_pdf_export_controller.ex
-#, elixir-autogen, elixir-format
-msgid "Export contains %{count} rows, maximum is %{max}"
-msgstr ""
-
-#: lib/mv_web/components/export_dropdown.ex
-#, elixir-autogen, elixir-format, fuzzy
-msgid "Export members to PDF"
-msgstr ""
-
-#: lib/mv_web/controllers/member_pdf_export_controller.ex
-#, elixir-autogen, elixir-format
-msgid "Failed to generate PDF export"
-msgstr ""
-
-#: lib/mv/membership/members_pdf.ex
-#, elixir-autogen, elixir-format
-msgid "Member %{club_name}"
-msgstr "Member %{club_name}"
-
-#: lib/mv/membership/members_pdf.ex
-#, elixir-autogen, elixir-format
-msgid "Member count:"
-msgstr "Member count:"
-
-#: lib/mv_web/components/export_dropdown.ex
-#, elixir-autogen, elixir-format
-msgid "PDF"
-msgstr ""
-
-#~ #: lib/mv_web/live/global_settings_live.ex
-#~ #, elixir-autogen, elixir-format, fuzzy
-#~ msgid "Custom Fields in CSV Import"
-#~ msgstr ""
diff --git a/priv/pdf_templates/members_export.typ b/priv/pdf_templates/members_export.typ
deleted file mode 100644
index 5dca208..0000000
--- a/priv/pdf_templates/members_export.typ
+++ /dev/null
@@ -1,95 +0,0 @@
-// Typst template for member export (PDF)
-// Expected sys.inputs.elixir_data:
-// {
-// "columns": [{"key": "...", "kind": "...", "label": "..."}, ...],
-// "rows": [["cell1", "cell2", ...], ...],
-// "meta": {"generated_at": "...", "member_count": 123}
-// }
-
-#set page(
- paper: "a4",
- flipped: true,
- margin: (top: 1.2cm, bottom: 1.2cm, left: 1.0cm, right: 1.0cm)
-)
-
-#set text(size: 9pt, hyphenate: true)
-#set heading(numbering: none)
-
-// Enable text wrapping in table cells
-#show table.cell: it => box(width: 100%)[#it]
-
-#let data = sys.inputs.elixir_data
-#let columns = data.at("columns", default: ())
-#let rows = data.at("rows", default: ())
-#let meta = data.at("meta", default: (generated_at: "", member_count: rows.len()))
-#let title = data.at("title", default: "Member Export")
-#let created_at_label = data.at("created_at_label", default: "Created at:")
-#let member_count_label = data.at("member_count_label", default: "Member count:")
-
-// Title
-#align(center)[
- #text(size: 14pt, weight: "bold")[#title]
-]
-
-#v(0.4cm)
-
-// Export metadata
-#set text(size: 8pt, fill: black)
-#grid(
- columns: (1fr, 1fr),
- gutter: 1cm,
- [*#created_at_label* #meta.at("generated_at", default: "")],
- [*#member_count_label* #meta.at("member_count", default: rows.len())],
-)
-
-#v(0.6cm)
-
-// ---- Horizontal paging config ----
-#let fixed_count = calc.min(2, columns.len())
-#let max_dynamic_cols = 5
-#let fixed_col_widths = (32mm, 32mm)
-
-#let fixed_cols = columns.slice(0, fixed_count)
-#let dynamic_cols = columns.slice(fixed_count, columns.len())
-#let dynamic_chunks = dynamic_cols.chunks(max_dynamic_cols)
-
-#let render_chunk(chunk_index, dyn_cols_chunk) = [
- #let dyn_count = dyn_cols_chunk.len()
- #let start = fixed_count + chunk_index * max_dynamic_cols
-
- #let page_cols = fixed_cols + dyn_cols_chunk
- #let headers = page_cols.map(c => c.at("label", default: ""))
-
- // widths: first two columns fixed (32mm, 42mm), rest distributed as 1fr
- #let widths = (
- if fixed_count >= 1 { fixed_col_widths.at(0) } else { 1fr },
- if fixed_count >= 2 { fixed_col_widths.at(1) } else { 1fr },
- ..((1fr,) * dyn_count)
- )
-
- #let header_cells = headers.map(h => text(weight: "bold", size: 9pt)[#h])
-
- // Body cells (row-major), nur die Spalten dieses Chunks
- #let body_cells = (
- rows
- .map(row => row.slice(0, fixed_count) + row.slice(start, start + dyn_count))
- .map(cells => cells.map(cell => text(size: 8.5pt)[#cell]))
- .flatten()
- )
-
- #table(
- columns: widths,
- table.header(..header_cells),
- ..body_cells,
- )
-]
-
-// ---- Output ----
-#if dynamic_cols.len() == 0 {
- render_chunk(0, ())
-} else {
- for (i, chunk) in dynamic_chunks.enumerate() {
- render_chunk(i, chunk)
- if i < dynamic_chunks.len() - 1 { pagebreak() }
- }
-}
diff --git a/test/mv/membership/member_export_build_test.exs b/test/mv/membership/member_export_build_test.exs
deleted file mode 100644
index 8b13789..0000000
--- a/test/mv/membership/member_export_build_test.exs
+++ /dev/null
@@ -1 +0,0 @@
-
diff --git a/test/mv/membership/members_pdf_test.exs b/test/mv/membership/members_pdf_test.exs
deleted file mode 100644
index 8b13789..0000000
--- a/test/mv/membership/members_pdf_test.exs
+++ /dev/null
@@ -1 +0,0 @@
-
diff --git a/test/mv_web/member_live/index_custom_fields_sorting_test.exs b/test/mv_web/member_live/index_custom_fields_sorting_test.exs
index 2f12fcc..c8201fd 100644
--- a/test/mv_web/member_live/index_custom_fields_sorting_test.exs
+++ b/test/mv_web/member_live/index_custom_fields_sorting_test.exs
@@ -145,10 +145,8 @@ defmodule MvWeb.MemberLive.IndexCustomFieldsSortingTest do
|> element("[data-testid='custom_field_#{field.id}']")
|> render_click()
- # Check URL was updated (param order may vary)
- path = assert_patch(view)
- assert path =~ "sort_order=desc"
- assert path =~ "sort_field=custom_field_#{field.id}"
+ # Check URL was updated
+ assert_patch(view, "/members?query=&sort_field=custom_field_#{field.id}&sort_order=desc")
# Verify sort state
assert has_element?(view, "[data-testid='custom_field_#{field.id}'][aria-label='descending']")
diff --git a/test/mv_web/member_live/index_groups_accessibility_test.exs b/test/mv_web/member_live/index_groups_accessibility_test.exs
deleted file mode 100644
index ab9b728..0000000
--- a/test/mv_web/member_live/index_groups_accessibility_test.exs
+++ /dev/null
@@ -1,178 +0,0 @@
-defmodule MvWeb.MemberLive.IndexGroupsAccessibilityTest do
- @moduledoc """
- Tests for accessibility of groups feature in the member overview.
-
- Tests cover:
- - Badges have role="status" and aria-label
- - Filter dropdown has aria-label
- - Sort header has aria-label for screen reader
- - Keyboard navigation works (Tab through filter, sort header)
- """
- # async: false to prevent PostgreSQL deadlocks when creating members and groups
- use MvWeb.ConnCase, async: false
- import Phoenix.LiveViewTest
- require Ash.Query
-
- alias Mv.Membership.{Group, MemberGroup}
-
- setup do
- system_actor = Mv.Helpers.SystemActor.get_system_actor()
-
- # Create test members
- {:ok, member1} =
- Mv.Membership.create_member(
- %{first_name: "Alice", last_name: "Anderson", email: "alice@example.com"},
- actor: system_actor
- )
-
- # Create test groups
- {:ok, group1} =
- Group
- |> Ash.Changeset.for_create(:create, %{name: "Board Members"})
- |> Ash.create(actor: system_actor)
-
- # Create member-group associations
- {:ok, _mg1} =
- MemberGroup
- |> Ash.Changeset.for_create(:create, %{member_id: member1.id, group_id: group1.id})
- |> Ash.create(actor: system_actor)
-
- %{
- member1: member1,
- group1: group1
- }
- end
-
- @tag :ui
- test "group badges have role and aria-label", %{
- conn: conn,
- member1: member1,
- group1: group1
- } do
- conn = conn_with_oidc_user(conn)
- {:ok, view, html} = live(conn, "/members")
-
- # Verify badges have role="status" and aria-label containing the group name
- assert has_element?(view, "span[role='status'][aria-label*='#{group1.name}']")
- assert html =~ group1.name
-
- # Verify member1's row contains the badge
- assert html =~ member1.first_name
- end
-
- @tag :ui
- test "filter dropdown has group presence section with legend", %{
- conn: conn
- } do
- conn = conn_with_oidc_user(conn)
- {:ok, view, _html} = live(conn, "/members")
-
- # Open filter dropdown
- view
- |> element("button[aria-label='Filter members']")
- |> render_click()
-
- html = render(view)
- # Groups section: legend "Member has groups" and radios (Any / Yes / No)
- assert html =~ ~r/[Gg]roups/
- assert has_element?(view, "[data-testid='member-filter-form']")
- end
-
- @tag :ui
- test "sort header has aria-label for screen reader", %{
- conn: conn
- } do
- conn = conn_with_oidc_user(conn)
- {:ok, view, _html} = live(conn, "/members")
-
- # Verify sort header has aria-label describing the sort state
- assert has_element?(view, "[data-testid='groups'][aria-label]")
- end
-
- @tag :ui
- test "keyboard navigation works for filter dropdown", %{
- conn: conn,
- member1: member1,
- group1: group1
- } do
- conn = conn_with_oidc_user(conn)
- {:ok, view, _html} = live(conn, "/members")
-
- view
- |> element("button[aria-label='Filter members']")
- |> render_click()
-
- view
- |> element("[data-testid='member-filter-form']")
- |> render_change(%{"group_#{group1.id}" => "in", "payment_filter" => "all"})
-
- html = render(view)
- assert html =~ member1.first_name
- end
-
- @tag :ui
- test "keyboard navigation works for sort header", %{
- conn: conn
- } do
- conn = conn_with_oidc_user(conn)
- {:ok, view, _html} = live(conn, "/members")
-
- assert has_element?(view, "[data-testid='groups']")
-
- view
- |> element("[data-testid='groups']")
- |> render_click()
-
- # Verify sort was applied (URL may include other params)
- assert has_element?(view, "[data-testid='groups'][aria-label*='ascending']")
- end
-
- @tag :ui
- test "screen reader announcements for filter changes", %{
- conn: conn,
- member1: member1,
- group1: group1
- } do
- conn = conn_with_oidc_user(conn)
- {:ok, view, _html} = live(conn, "/members")
-
- view
- |> element("button[aria-label='Filter members']")
- |> render_click()
-
- view
- |> element("[data-testid='member-filter-form']")
- |> render_change(%{"group_#{group1.id}" => "in", "payment_filter" => "all"})
-
- html = render(view)
- assert html =~ member1.first_name
- end
-
- @tag :ui
- test "multiple badges are announced correctly", %{
- conn: conn,
- member1: member1
- } do
- system_actor = Mv.Helpers.SystemActor.get_system_actor()
-
- # Create multiple groups for member1
- {:ok, group2} =
- Group
- |> Ash.Changeset.for_create(:create, %{name: "Active Members"})
- |> Ash.create(actor: system_actor)
-
- {:ok, _mg} =
- MemberGroup
- |> Ash.Changeset.for_create(:create, %{member_id: member1.id, group_id: group2.id})
- |> Ash.create(actor: system_actor)
-
- conn = conn_with_oidc_user(conn)
- {:ok, _view, html} = live(conn, "/members")
-
- # Verify multiple badges are present
- assert html =~ member1.first_name
- # Both groups should be visible
- # Screen reader should be able to distinguish between multiple badges
- assert html
- end
-end
diff --git a/test/mv_web/member_live/index_groups_display_test.exs b/test/mv_web/member_live/index_groups_display_test.exs
deleted file mode 100644
index b28b978..0000000
--- a/test/mv_web/member_live/index_groups_display_test.exs
+++ /dev/null
@@ -1,103 +0,0 @@
-defmodule MvWeb.MemberLive.IndexGroupsDisplayTest do
- @moduledoc """
- Tests for displaying groups in the member overview.
-
- Tests cover:
- - Group badges are displayed for members in groups
- - Multiple badges for members in multiple groups
- - No badge for members without groups
- - Badge shows group name correctly
- """
- # async: false to prevent PostgreSQL deadlocks when creating members and groups
- use MvWeb.ConnCase, async: false
- import Phoenix.LiveViewTest
- require Ash.Query
-
- alias Mv.Membership.{Group, MemberGroup}
-
- setup do
- system_actor = Mv.Helpers.SystemActor.get_system_actor()
-
- {:ok, member1} =
- Mv.Membership.create_member(
- %{first_name: "Alice", last_name: "Anderson", email: "alice@example.com"},
- actor: system_actor
- )
-
- {:ok, member2} =
- Mv.Membership.create_member(
- %{first_name: "Bob", last_name: "Brown", email: "bob@example.com"},
- actor: system_actor
- )
-
- {:ok, member3} =
- Mv.Membership.create_member(
- %{first_name: "Charlie", last_name: "Clark", email: "charlie@example.com"},
- actor: system_actor
- )
-
- {:ok, group1} =
- Group
- |> Ash.Changeset.for_create(:create, %{name: "Board Members"})
- |> Ash.create(actor: system_actor)
-
- {:ok, group2} =
- Group
- |> Ash.Changeset.for_create(:create, %{name: "Active Members"})
- |> Ash.create(actor: system_actor)
-
- {:ok, _mg1} =
- MemberGroup
- |> Ash.Changeset.for_create(:create, %{member_id: member1.id, group_id: group1.id})
- |> Ash.create(actor: system_actor)
-
- {:ok, _mg2} =
- MemberGroup
- |> Ash.Changeset.for_create(:create, %{member_id: member1.id, group_id: group2.id})
- |> Ash.create(actor: system_actor)
-
- {:ok, _mg3} =
- MemberGroup
- |> Ash.Changeset.for_create(:create, %{member_id: member2.id, group_id: group1.id})
- |> Ash.create(actor: system_actor)
-
- %{member1: member1, member2: member2, member3: member3, group1: group1, group2: group2}
- end
-
- test "displays group badges for members in groups", %{
- conn: conn,
- group1: group1,
- group2: group2
- } do
- conn = conn_with_oidc_user(conn)
- {:ok, _view, html} = live(conn, "/members")
-
- assert html =~ group1.name
- assert html =~ group2.name
- end
-
- test "displays multiple badges for member in multiple groups", %{
- conn: conn,
- member1: member1,
- group1: group1,
- group2: group2
- } do
- conn = conn_with_oidc_user(conn)
- {:ok, _view, html} = live(conn, "/members")
- assert html =~ member1.first_name
- assert html =~ group1.name
- assert html =~ group2.name
- end
-
- test "shows placeholder for members without groups", %{conn: conn, member3: member3} do
- conn = conn_with_oidc_user(conn)
- {:ok, _view, html} = live(conn, "/members")
- assert html =~ member3.first_name
- end
-
- test "displays group name correctly in badge", %{conn: conn, group1: group1} do
- conn = conn_with_oidc_user(conn)
- {:ok, _view, html} = live(conn, "/members")
- assert html =~ group1.name
- end
-end
diff --git a/test/mv_web/member_live/index_groups_filter_test.exs b/test/mv_web/member_live/index_groups_filter_test.exs
deleted file mode 100644
index 782ab33..0000000
--- a/test/mv_web/member_live/index_groups_filter_test.exs
+++ /dev/null
@@ -1,161 +0,0 @@
-defmodule MvWeb.MemberLive.IndexGroupsFilterTest do
- @moduledoc """
- Tests for filtering members by group in the member overview.
-
- Uses the filter dropdown (MemberFilterComponent) with one row per group:
- All / Yes / No (per group). Multiple active group filters combine with AND
- (member must match all selected group conditions).
- """
- # async: false to prevent PostgreSQL deadlocks when creating members and groups
- use MvWeb.ConnCase, async: false
- import Phoenix.LiveViewTest
- require Ash.Query
-
- alias Mv.Membership.{Group, MemberGroup}
-
- setup do
- system_actor = Mv.Helpers.SystemActor.get_system_actor()
-
- {:ok, member1} =
- Mv.Membership.create_member(
- %{first_name: "Alice", last_name: "Anderson", email: "alice@example.com"},
- actor: system_actor
- )
-
- {:ok, member2} =
- Mv.Membership.create_member(
- %{first_name: "Bob", last_name: "Brown", email: "bob@example.com"},
- actor: system_actor
- )
-
- {:ok, member3} =
- Mv.Membership.create_member(
- %{first_name: "Charlie", last_name: "Clark", email: "charlie@example.com"},
- actor: system_actor
- )
-
- {:ok, group1} =
- Group
- |> Ash.Changeset.for_create(:create, %{name: "Board Members"})
- |> Ash.create(actor: system_actor)
-
- {:ok, group2} =
- Group
- |> Ash.Changeset.for_create(:create, %{name: "Active Members"})
- |> Ash.create(actor: system_actor)
-
- {:ok, _mg1} =
- MemberGroup
- |> Ash.Changeset.for_create(:create, %{member_id: member1.id, group_id: group1.id})
- |> Ash.create(actor: system_actor)
-
- {:ok, _mg2} =
- MemberGroup
- |> Ash.Changeset.for_create(:create, %{member_id: member2.id, group_id: group2.id})
- |> Ash.create(actor: system_actor)
-
- %{member1: member1, member2: member2, member3: member3, group1: group1, group2: group2}
- end
-
- defp open_filter_and_set_group(view, group_id, value) do
- view
- |> element("button[aria-label='Filter members']")
- |> render_click()
-
- key = "group_#{group_id}"
-
- view
- |> element("[data-testid='member-filter-form']")
- |> render_change(%{key => value, "payment_filter" => "all"})
-
- # Force LiveView to process {:group_filter_changed, ...} (render triggers mailbox processing)
- _ = render(view)
- assert_patch(view)
- end
-
- test "filter All (default) shows all members", %{
- conn: conn,
- member1: m1,
- member2: m2,
- member3: m3
- } do
- conn = conn_with_oidc_user(conn)
- {:ok, _view, html} = live(conn, "/members")
- assert html =~ m1.first_name
- assert html =~ m2.first_name
- assert html =~ m3.first_name
- end
-
- test "filter group1 Yes shows only members in group1", %{
- conn: conn,
- member1: m1,
- member2: m2,
- member3: m3,
- group1: group1
- } do
- conn = conn_with_oidc_user(conn)
- {:ok, view, _html} = live(conn, "/members")
-
- open_filter_and_set_group(view, group1.id, "in")
-
- html = render(view)
- assert html =~ m1.first_name
- refute html =~ m2.first_name
- refute html =~ m3.first_name
- end
-
- test "filter group1 No shows only members not in group1", %{
- conn: conn,
- member1: m1,
- member2: m2,
- member3: m3,
- group1: group1
- } do
- conn = conn_with_oidc_user(conn)
- {:ok, view, _html} = live(conn, "/members")
-
- open_filter_and_set_group(view, group1.id, "not_in")
-
- html = render(view)
- refute html =~ m1.first_name
- assert html =~ m2.first_name
- assert html =~ m3.first_name
- end
-
- test "filter persists in URL parameters", %{
- conn: conn,
- member1: m1,
- member2: m2,
- member3: m3,
- group1: group1
- } do
- conn = conn_with_oidc_user(conn)
- {:ok, view, _html} = live(conn, "/members")
-
- open_filter_and_set_group(view, group1.id, "in")
-
- html = render(view)
- assert html =~ m1.first_name
- refute html =~ m2.first_name
- refute html =~ m3.first_name
-
- {:ok, _view2, html2} = live(conn, "/members?group_#{group1.id}=in")
- assert html2 =~ m1.first_name
- refute html2 =~ m2.first_name
- refute html2 =~ m3.first_name
- end
-
- test "filter is restored from URL on load", %{
- conn: conn,
- member1: m1,
- member2: m2,
- member3: m3,
- group1: group1
- } do
- conn = conn_with_oidc_user(conn)
- {:ok, _view, html} = live(conn, "/members?group_#{group1.id}=in")
- assert html =~ m1.first_name
- refute html =~ m2.first_name
- refute html =~ m3.first_name
- end
-end
diff --git a/test/mv_web/member_live/index_groups_integration_test.exs b/test/mv_web/member_live/index_groups_integration_test.exs
deleted file mode 100644
index 3075d54..0000000
--- a/test/mv_web/member_live/index_groups_integration_test.exs
+++ /dev/null
@@ -1,247 +0,0 @@
-defmodule MvWeb.MemberLive.IndexGroupsIntegrationTest do
- @moduledoc """
- Tests for integration of groups with existing features in the member overview.
-
- Tests cover:
- - Groups column works with Field Visibility (column can be hidden)
- - Groups filter works with Custom Field filters
- - Groups sorting works with other sortings
- - Groups work with Membership Fee Status filter
- - Groups work with existing search (but not testing search integration itself)
- """
- # async: false to prevent PostgreSQL deadlocks when creating members and groups
- use MvWeb.ConnCase, async: false
- import Phoenix.LiveViewTest
- require Ash.Query
-
- alias Mv.Membership.{Group, MemberGroup, CustomField, CustomFieldValue}
-
- setup do
- system_actor = Mv.Helpers.SystemActor.get_system_actor()
-
- # Create test members
- {:ok, member1} =
- Mv.Membership.create_member(
- %{first_name: "Alice", last_name: "Anderson", email: "alice@example.com"},
- actor: system_actor
- )
-
- {:ok, member2} =
- Mv.Membership.create_member(
- %{first_name: "Bob", last_name: "Brown", email: "bob@example.com"},
- actor: system_actor
- )
-
- # Create test groups
- {:ok, group1} =
- Group
- |> Ash.Changeset.for_create(:create, %{name: "Board Members"})
- |> Ash.create(actor: system_actor)
-
- # Create member-group associations
- {:ok, _mg1} =
- MemberGroup
- |> Ash.Changeset.for_create(:create, %{member_id: member1.id, group_id: group1.id})
- |> Ash.create(actor: system_actor)
-
- # Create custom field for filter integration test
- {:ok, custom_field} =
- CustomField
- |> Ash.Changeset.for_create(:create, %{
- name: "newsletter",
- value_type: :boolean,
- show_in_overview: false
- })
- |> Ash.create(actor: system_actor)
-
- # Create custom field value for member1
- {:ok, _cfv} =
- CustomFieldValue
- |> Ash.Changeset.for_create(:create, %{
- member_id: member1.id,
- custom_field_id: custom_field.id,
- value: %{"_union_type" => "boolean", "_union_value" => true}
- })
- |> Ash.create(actor: system_actor)
-
- %{
- member1: member1,
- member2: member2,
- group1: group1,
- custom_field: custom_field
- }
- end
-
- test "groups column works with field visibility", %{
- conn: conn,
- member1: member1,
- group1: group1
- } do
- conn = conn_with_oidc_user(conn)
- {:ok, _view, html} = live(conn, "/members")
-
- # Verify groups column is visible by default
- assert html =~ group1.name
- assert html =~ member1.first_name
-
- # Hide groups column via field visibility dropdown
- # (This tests integration with field visibility feature)
- # Note: Actual implementation depends on how field visibility works
- # For now, we verify the column exists and can be toggled
- assert html
- end
-
- test "groups filter works with custom field filters", %{
- conn: conn,
- member1: member1,
- group1: group1
- } do
- # Verify group filter applies; boolean filters live in the filter dropdown and
- # are exercised in member filter tests. Here we only assert group filter works.
- conn = conn_with_oidc_user(conn)
- {:ok, view, _html} = live(conn, "/members")
-
- view
- |> element("button[aria-label='Filter members']")
- |> render_click()
-
- view
- |> element("[data-testid='member-filter-form']")
- |> render_change(%{"group_#{group1.id}" => "in", "payment_filter" => "all"})
-
- html = render(view)
- assert html =~ member1.first_name
- end
-
- test "groups sorting works with other sortings", %{
- conn: conn,
- member1: member1,
- member2: member2
- } do
- conn = conn_with_oidc_user(conn)
- {:ok, view, _html} = live(conn, "/members?sort_field=first_name&sort_order=asc")
-
- # Apply groups sorting (should combine with existing sort)
- view
- |> element("[data-testid='groups']")
- |> render_click()
-
- # Verify both sorts are applied (or groups sort replaces first_name sort)
- html = render(view)
- assert html =~ member1.first_name
- assert html =~ member2.first_name
-
- # Sort by groups was applied (URL may include query= and other default params)
- assert has_element?(view, "[data-testid='groups'][aria-label*='ascending']")
- end
-
- test "groups work with membership fee status filter", %{
- conn: conn,
- member1: member1,
- group1: group1
- } do
- system_actor = Mv.Helpers.SystemActor.get_system_actor()
-
- # Create a membership fee type and cycle for member1
- {:ok, fee_type} =
- Mv.MembershipFees.MembershipFeeType
- |> Ash.Changeset.for_create(:create, %{
- name: "Test Fee",
- amount: Decimal.new("50.00"),
- interval: :yearly
- })
- |> Ash.create(actor: system_actor)
-
- # Set member's fee type so get_last_completed_cycle finds the cycle (uses member.membership_fee_type)
- {:ok, _member1} =
- Mv.Membership.update_member(member1, %{membership_fee_type_id: fee_type.id},
- actor: system_actor
- )
-
- {:ok, _cycle} =
- Mv.MembershipFees.MembershipFeeCycle
- |> Ash.Changeset.for_create(:create, %{
- member_id: member1.id,
- membership_fee_type_id: fee_type.id,
- cycle_start: ~D[2024-01-01],
- amount: Decimal.new("50.00"),
- status: :paid
- })
- |> Ash.create(actor: system_actor)
-
- conn = conn_with_oidc_user(conn)
-
- {:ok, _view, html} =
- live(conn, "/members?group_#{group1.id}=in&cycle_status_filter=paid")
-
- assert html =~ "Members"
- # member1 has a group and a paid cycle; page should load with both filters
- assert html =~ member1.first_name
- end
-
- test "groups work with existing search (not testing search integration)", %{
- conn: conn,
- member1: member1,
- member2: member2,
- group1: group1
- } do
- conn = conn_with_oidc_user(conn)
- {:ok, view, _html} = live(conn, "/members")
-
- # Apply group filter
- view
- |> element("button[aria-label='Filter members']")
- |> render_click()
-
- view
- |> element("[data-testid='member-filter-form']")
- |> render_change(%{"group_#{group1.id}" => "in", "payment_filter" => "all"})
-
- # Apply search (this tests that filter and search work together;
- # search form is in SearchBarComponent with phx-submit="search")
- view
- |> element("form[phx-submit='search']")
- |> render_submit(%{"query" => "Alice"})
-
- # Verify filter and search both work
- html = render(view)
- assert html =~ member1.first_name
- refute html =~ member2.first_name
-
- # Note: We're not testing that group names are searchable
- # (that's part of Issue #5 - Search Integration)
- end
-
- test "all filters and sortings work together", %{
- conn: conn,
- member1: member1,
- group1: group1
- } do
- conn = conn_with_oidc_user(conn)
- {:ok, view, _html} = live(conn, "/members")
-
- # Apply group filter
- view
- |> element("button[aria-label='Filter members']")
- |> render_click()
-
- view
- |> element("[data-testid='member-filter-form']")
- |> render_change(%{"group_#{group1.id}" => "in", "payment_filter" => "all"})
-
- # Apply sorting
- view
- |> element("[data-testid='groups']")
- |> render_click()
-
- # Apply search
- view
- |> element("form[phx-submit='search']")
- |> render_submit(%{"query" => "Alice"})
-
- # Verify group filter, sort, and search are all applied
- html = render(view)
- assert html =~ member1.first_name
- assert has_element?(view, "[data-testid='groups'][aria-label*='ascending']")
- end
-end
diff --git a/test/mv_web/member_live/index_groups_performance_test.exs b/test/mv_web/member_live/index_groups_performance_test.exs
deleted file mode 100644
index 761c4eb..0000000
--- a/test/mv_web/member_live/index_groups_performance_test.exs
+++ /dev/null
@@ -1,207 +0,0 @@
-defmodule MvWeb.MemberLive.IndexGroupsPerformanceTest do
- @moduledoc """
- Tests for performance and N+1 query prevention for groups in the member overview.
-
- Tests cover:
- - Groups are loaded with members in a single query (preloading)
- - No N+1 queries when loading members with groups
- - Filter works at database level (not in-memory)
- - Sort runs in-memory but uses preloaded group data (no extra DB queries)
- """
- # async: false to prevent PostgreSQL deadlocks when creating members and groups
- use MvWeb.ConnCase, async: false
- import Phoenix.LiveViewTest
- require Ash.Query
-
- alias Mv.Membership.{Group, MemberGroup}
-
- setup do
- system_actor = Mv.Helpers.SystemActor.get_system_actor()
-
- # Create test members (enough to test performance)
- members =
- for i <- 1..10 do
- {:ok, member} =
- Mv.Membership.create_member(
- %{
- first_name: "Member#{i}",
- last_name: "Test#{i}",
- email: "member#{i}@example.com"
- },
- actor: system_actor
- )
-
- member
- end
-
- # Create test groups
- {:ok, group1} =
- Group
- |> Ash.Changeset.for_create(:create, %{name: "Group 1"})
- |> Ash.create(actor: system_actor)
-
- {:ok, group2} =
- Group
- |> Ash.Changeset.for_create(:create, %{name: "Group 2"})
- |> Ash.create(actor: system_actor)
-
- # Assign members to groups (alternating pattern)
- Enum.each(Enum.with_index(members), fn {member, index} ->
- group_id = if rem(index, 2) == 0, do: group1.id, else: group2.id
-
- {:ok, _mg} =
- MemberGroup
- |> Ash.Changeset.for_create(:create, %{member_id: member.id, group_id: group_id})
- |> Ash.create(actor: system_actor)
- end)
-
- %{
- members: members,
- group1: group1,
- group2: group2
- }
- end
-
- @tag :slow
- test "groups are preloaded with members (no N+1 queries)", %{
- conn: conn,
- members: _members
- } do
- # This test verifies that groups are loaded efficiently
- # We check query count by monitoring database queries
- # Note: Actual query counting would require Ecto query logging
- # For now, we verify the functionality works correctly
-
- conn = conn_with_oidc_user(conn)
- {:ok, _view, html} = live(conn, "/members")
-
- # Verify all members are loaded
- Enum.each(1..10, fn i ->
- assert html =~ "Member#{i}"
- end)
-
- # Verify groups are displayed (if preloaded correctly, this should work)
- # If N+1 queries occurred, the page might be slow or fail
- assert html
- end
-
- @tag :slow
- test "filter works at database level", %{
- conn: conn,
- group1: group1,
- members: members
- } do
- # This test verifies that filtering happens in the database query,
- # not by filtering in-memory after loading all members
-
- conn = conn_with_oidc_user(conn)
- {:ok, view, _html} = live(conn, "/members")
-
- # Open filter and apply "Yes" for group1 (even-indexed members are in group1)
- view
- |> element("button[aria-label='Filter members']")
- |> render_click()
-
- view
- |> element("[data-testid='member-filter-form']")
- |> render_change(%{"group_#{group1.id}" => "in", "payment_filter" => "all"})
-
- # Force LiveView to process {:group_filter_changed, ...}
- html = render(view)
-
- # Only even-indexed members (0,2,4,6,8) are in group1
- Enum.each([0, 2, 4, 6, 8], fn i ->
- member = Enum.at(members, i)
- assert html =~ member.first_name
- end)
-
- Enum.each([1, 3, 5, 7, 9], fn i ->
- member = Enum.at(members, i)
- refute html =~ member.first_name
- end)
- end
-
- @tag :slow
- test "sorting works at database level", %{
- conn: conn,
- members: _members
- } do
- # This test verifies that sorting happens in the database query,
- # not by sorting in-memory after loading all members
-
- conn = conn_with_oidc_user(conn)
- {:ok, view, _html} = live(conn, "/members")
-
- # Apply sorting
- view
- |> element("[data-testid='groups']")
- |> render_click()
-
- # Verify sorting is applied
- html = render(view)
-
- # Verify members are displayed (if sorting was done in-memory,
- # we'd load all members first, which is less efficient)
- assert html
-
- # Database-level sorting is more efficient for large datasets
- end
-
- @tag :slow
- test "handles many members with many groups efficiently", %{
- conn: conn
- } do
- system_actor = Mv.Helpers.SystemActor.get_system_actor()
-
- # Create many members (20) with multiple groups each (use distinct emails to avoid collision with setup)
- members =
- for i <- 11..30 do
- {:ok, member} =
- Mv.Membership.create_member(
- %{
- first_name: "Member#{i}",
- last_name: "Test#{i}",
- email: "member#{i}@example.com"
- },
- actor: system_actor
- )
-
- member
- end
-
- # Create multiple groups (use distinct names to avoid collision with setup's Group 1/2)
- groups =
- for i <- 1..5 do
- {:ok, group} =
- Group
- |> Ash.Changeset.for_create(:create, %{name: "Perf Group #{i}"})
- |> Ash.create(actor: system_actor)
-
- group
- end
-
- # Assign each member to 2-3 random groups
- Enum.each(members, fn member ->
- selected_groups = Enum.take_random(groups, Enum.random(2..3))
-
- Enum.each(selected_groups, fn group ->
- {:ok, _mg} =
- MemberGroup
- |> Ash.Changeset.for_create(:create, %{member_id: member.id, group_id: group.id})
- |> Ash.create(actor: system_actor)
- end)
- end)
-
- conn = conn_with_oidc_user(conn)
- {:ok, _view, html} = live(conn, "/members")
-
- # Verify all members are loaded efficiently
- Enum.each(11..30, fn i ->
- assert html =~ "Member#{i}"
- end)
-
- # If preloading works correctly, this should be fast
- # If N+1 queries occurred, this would be very slow
- assert html
- end
-end
diff --git a/test/mv_web/member_live/index_groups_sorting_test.exs b/test/mv_web/member_live/index_groups_sorting_test.exs
deleted file mode 100644
index 068152c..0000000
--- a/test/mv_web/member_live/index_groups_sorting_test.exs
+++ /dev/null
@@ -1,69 +0,0 @@
-defmodule MvWeb.MemberLive.IndexGroupsSortingTest do
- @moduledoc """
- Tests for sorting by groups in the member overview.
- """
- # async: false to prevent PostgreSQL deadlocks when creating members and groups
- use MvWeb.ConnCase, async: false
- import Phoenix.LiveViewTest
- require Ash.Query
-
- alias Mv.Membership.{Group, MemberGroup}
-
- setup do
- system_actor = Mv.Helpers.SystemActor.get_system_actor()
-
- {:ok, member1} =
- Mv.Membership.create_member(
- %{first_name: "Alice", last_name: "Anderson", email: "alice@example.com"},
- actor: system_actor
- )
-
- {:ok, member2} =
- Mv.Membership.create_member(
- %{first_name: "Bob", last_name: "Brown", email: "bob@example.com"},
- actor: system_actor
- )
-
- {:ok, member4} =
- Mv.Membership.create_member(
- %{first_name: "David", last_name: "Davis", email: "david@example.com"},
- actor: system_actor
- )
-
- {:ok, group_a} =
- Group
- |> Ash.Changeset.for_create(:create, %{name: "A Group"})
- |> Ash.create(actor: system_actor)
-
- {:ok, group_b} =
- Group
- |> Ash.Changeset.for_create(:create, %{name: "B Group"})
- |> Ash.create(actor: system_actor)
-
- {:ok, _mg1} =
- MemberGroup
- |> Ash.Changeset.for_create(:create, %{member_id: member1.id, group_id: group_a.id})
- |> Ash.create(actor: system_actor)
-
- {:ok, _mg2} =
- MemberGroup
- |> Ash.Changeset.for_create(:create, %{member_id: member2.id, group_id: group_b.id})
- |> Ash.create(actor: system_actor)
-
- %{member1: member1, member2: member2, member4: member4, group_a: group_a, group_b: group_b}
- end
-
- test "sorts by group name ascending", %{conn: conn, group_a: group_a} do
- conn = conn_with_oidc_user(conn)
- {:ok, view, _html} = live(conn, "/members")
-
- view
- |> element("[data-testid='groups']")
- |> render_click()
-
- # Sort was applied: button shows ascending state and group names still visible
- assert has_element?(view, "[data-testid='groups']")
- html = render(view)
- assert html =~ group_a.name
- end
-end
diff --git a/test/mv_web/member_live/index_groups_url_params_test.exs b/test/mv_web/member_live/index_groups_url_params_test.exs
deleted file mode 100644
index 469b010..0000000
--- a/test/mv_web/member_live/index_groups_url_params_test.exs
+++ /dev/null
@@ -1,185 +0,0 @@
-defmodule MvWeb.MemberLive.IndexGroupsUrlParamsTest do
- @moduledoc """
- Tests for URL parameter persistence for groups in the member overview.
-
- Tests cover:
- - Group presence filter is written to URL (group_presence=has_groups|no_groups)
- - Group sorting is written to URL (sort_field=groups&sort_order=asc)
- - URL parameters are restored on load
- - URL parameters work with other parameters (query, sort_field, etc.)
- - URL is bookmarkable (filter/sorting persist)
- """
- # async: false to prevent PostgreSQL deadlocks when creating members and groups
- use MvWeb.ConnCase, async: false
- import Phoenix.LiveViewTest
- require Ash.Query
-
- alias Mv.Membership.{Group, MemberGroup}
-
- setup do
- system_actor = Mv.Helpers.SystemActor.get_system_actor()
-
- # Create test members
- {:ok, member1} =
- Mv.Membership.create_member(
- %{first_name: "Alice", last_name: "Anderson", email: "alice@example.com"},
- actor: system_actor
- )
-
- {:ok, member2} =
- Mv.Membership.create_member(
- %{first_name: "Bob", last_name: "Brown", email: "bob@example.com"},
- actor: system_actor
- )
-
- # Create test groups
- {:ok, group1} =
- Group
- |> Ash.Changeset.for_create(:create, %{name: "Board Members"})
- |> Ash.create(actor: system_actor)
-
- # Create member-group associations
- {:ok, _mg1} =
- MemberGroup
- |> Ash.Changeset.for_create(:create, %{member_id: member1.id, group_id: group1.id})
- |> Ash.create(actor: system_actor)
-
- %{
- member1: member1,
- member2: member2,
- group1: group1
- }
- end
-
- test "group filter is written to URL", %{
- conn: conn,
- member1: member1,
- group1: group1
- } do
- conn = conn_with_oidc_user(conn)
- {:ok, view, _html} = live(conn, "/members")
-
- view
- |> element("button[aria-label='Filter members']")
- |> render_click()
-
- view
- |> element("[data-testid='member-filter-form']")
- |> render_change(%{"group_#{group1.id}" => "in", "payment_filter" => "all"})
-
- html = render(view)
- assert html =~ member1.first_name
- end
-
- test "group sorting is written to URL", %{
- conn: conn
- } do
- conn = conn_with_oidc_user(conn)
- {:ok, view, _html} = live(conn, "/members")
-
- # Click on groups column header to sort
- view
- |> element("[data-testid='groups']")
- |> render_click()
-
- # Verify sort was applied (URL is patched with sort params)
- assert has_element?(view, "[data-testid='groups'][aria-label*='ascending']")
- end
-
- test "URL parameters are restored on load", %{
- conn: conn,
- member1: member1,
- member2: member2,
- group1: group1
- } do
- conn = conn_with_oidc_user(conn)
-
- {:ok, view, html} =
- live(conn, "/members?group_#{group1.id}=in&sort_field=groups&sort_order=asc")
-
- assert html =~ member1.first_name
- refute html =~ member2.first_name
- assert has_element?(view, "[data-testid='groups'][aria-label*='ascending']")
- end
-
- test "URL parameters work with query parameter", %{
- conn: conn,
- member1: member1,
- group1: group1
- } do
- conn = conn_with_oidc_user(conn)
- {:ok, _view, html} = live(conn, "/members?query=Alice&group_#{group1.id}=in")
-
- assert html =~ member1.first_name
- end
-
- test "URL parameters work with other sort fields", %{
- conn: conn,
- member1: member1,
- group1: group1
- } do
- conn = conn_with_oidc_user(conn)
-
- {:ok, view, html} =
- live(conn, "/members?sort_field=first_name&sort_order=desc&group_#{group1.id}=in")
-
- assert html =~ member1.first_name
- assert has_element?(view, "[data-testid='first_name'][aria-label*='descending']")
- end
-
- test "URL is bookmarkable with filter and sorting", %{
- conn: conn,
- member1: member1,
- group1: group1
- } do
- conn = conn_with_oidc_user(conn)
- bookmark_url = "/members?group_#{group1.id}=in&sort_field=groups&sort_order=asc"
-
- {:ok, view, html} = live(conn, bookmark_url)
-
- assert html =~ member1.first_name
- assert has_element?(view, "[data-testid='groups'][aria-label*='ascending']")
- end
-
- test "handles multiple group filter parameters (uses last one)", %{
- conn: conn,
- member1: member1,
- member2: member2,
- group1: group1
- } do
- conn = conn_with_oidc_user(conn)
- # Duplicate param for same group: last wins. group_id=in then not_in -> not_in
- {:ok, _view, html} =
- live(conn, "/members?group_#{group1.id}=in&group_#{group1.id}=not_in")
-
- # not_in group1: member2 and member3 (member1 is in group1)
- refute html =~ member1.first_name
- assert html =~ member2.first_name
- end
-
- test "handles invalid URL parameters gracefully", %{
- conn: conn,
- member1: member1,
- member2: member2
- } do
- conn = conn_with_oidc_user(conn)
- invalid_id = Ecto.UUID.generate()
- {:ok, _view, html} = live(conn, "/members?group_#{invalid_id}=in")
-
- # Unknown group id ignored, all members shown
- assert html =~ member1.first_name
- assert html =~ member2.first_name
- end
-
- test "handles malformed URL parameters", %{
- conn: conn,
- member1: member1,
- member2: member2
- } do
- conn = conn_with_oidc_user(conn)
- {:ok, _view, html} = live(conn, "/members?group_not-a-uuid=in")
-
- assert html =~ member1.first_name
- assert html =~ member2.first_name
- end
-end
diff --git a/test/mv_web/member_live/index_test.exs b/test/mv_web/member_live/index_test.exs
index 4f36795..9d4a429 100644
--- a/test/mv_web/member_live/index_test.exs
+++ b/test/mv_web/member_live/index_test.exs
@@ -522,7 +522,7 @@ defmodule MvWeb.MemberLive.IndexTest do
end
end
- describe "export dropdown" do
+ describe "export to CSV" do
setup do
system_actor = Mv.Helpers.SystemActor.get_system_actor()
@@ -535,139 +535,34 @@ defmodule MvWeb.MemberLive.IndexTest do
%{member1: m1}
end
- test "export dropdown button is rendered when no selection and shows (all)", %{conn: conn} do
+ test "export button is rendered when no selection and shows (all)", %{conn: conn} do
conn = conn_with_oidc_user(conn)
{:ok, _view, html} = live(conn, "/members")
- # Dropdown button should be present
- assert html =~ ~s(data-testid="export-dropdown")
- assert html =~ ~s(data-testid="export-dropdown-button")
- assert html =~ "Export"
# Button text shows "all" when 0 selected (locale-dependent)
+ assert html =~ "Export to CSV"
assert html =~ "all" or html =~ "All"
end
- test "after select_member event export dropdown shows (1)", %{conn: conn, member1: member1} do
+ test "after select_member event export button shows (1)", %{conn: conn, member1: member1} do
conn = conn_with_oidc_user(conn)
{:ok, view, _html} = live(conn, "/members")
render_click(view, "select_member", %{"id" => member1.id})
html = render(view)
- assert html =~ "Export"
+ assert html =~ "Export to CSV"
assert html =~ "(1)"
end
- test "dropdown opens and closes on click", %{conn: conn} do
+ test "form has correct action and payload hidden input", %{conn: conn} do
conn = conn_with_oidc_user(conn)
- {:ok, view, _html} = live(conn, "/members")
+ {:ok, _view, html} = live(conn, "/members")
- # Initially closed
- refute has_element?(view, ~s([data-testid="export-dropdown-menu"]))
-
- # Click to open
- view
- |> element(~s([data-testid="export-dropdown-button"]))
- |> render_click()
-
- # Menu should be visible
- assert has_element?(view, ~s([data-testid="export-dropdown-menu"]))
-
- # Click to close
- view
- |> element(~s([data-testid="export-dropdown-button"]))
- |> render_click()
-
- # Menu should be hidden
- refute has_element?(view, ~s([data-testid="export-dropdown-menu"]))
- end
-
- test "dropdown has click-away and ESC handlers", %{conn: conn} do
- conn = conn_with_oidc_user(conn)
- {:ok, view, _html} = live(conn, "/members")
-
- # Open dropdown
- view
- |> element(~s([data-testid="export-dropdown-button"]))
- |> render_click()
-
- html = render(view)
- assert has_element?(view, ~s([data-testid="export-dropdown-menu"]))
-
- # Check that click-away handler is present
- assert html =~ ~s(phx-click-away="close_dropdown")
- # Check that ESC handler is present
- assert html =~ ~s(phx-window-keydown="close_dropdown")
- assert html =~ ~s(phx-key="Escape")
- end
-
- test "dropdown menu contains CSV and PDF export links with correct payload", %{conn: conn} do
- conn = conn_with_oidc_user(conn)
- {:ok, view, _html} = live(conn, "/members")
-
- # Open dropdown
- view
- |> element(~s([data-testid="export-dropdown-button"]))
- |> render_click()
-
- html = render(view)
-
- # Check CSV link
- assert html =~ ~s(data-testid="export-csv-link")
assert html =~ "/members/export.csv"
assert html =~ ~s(name="payload")
assert html =~ ~s(type="hidden")
assert html =~ ~s(name="_csrf_token")
-
- # Check PDF link
- assert html =~ ~s(data-testid="export-pdf-link")
- assert html =~ "/members/export.pdf"
- assert html =~ ~s(name="payload")
- assert html =~ ~s(type="hidden")
- assert html =~ ~s(name="_csrf_token")
-
- # Both forms should have the same payload
- csv_form_payload = extract_payload_from_form(html, "/members/export.csv")
- pdf_form_payload = extract_payload_from_form(html, "/members/export.pdf")
-
- assert csv_form_payload == pdf_form_payload
- assert csv_form_payload != nil
- end
-
- test "dropdown has correct ARIA attributes", %{conn: conn} do
- conn = conn_with_oidc_user(conn)
- {:ok, view, _html} = live(conn, "/members")
-
- html = render(view)
-
- # Button should have aria-haspopup="menu"
- assert html =~ ~s(aria-haspopup="menu")
- # Button should have aria-expanded="false" when closed
- assert html =~ ~s(aria-expanded="false")
- # Button should have aria-controls pointing to menu
- assert html =~ ~s(aria-controls="export-dropdown-menu")
-
- # Open dropdown
- view
- |> element(~s([data-testid="export-dropdown-button"]))
- |> render_click()
-
- html = render(view)
- # Button should have aria-expanded="true" when open
- assert html =~ ~s(aria-expanded="true")
- # Menu should have role="menu"
- assert html =~ ~s(role="menu")
- end
-
- # Helper to extract payload value from form HTML
- defp extract_payload_from_form(html, action_path) do
- case Regex.run(
- ~r/