fix: sorting and filter for export

This commit is contained in:
carla 2026-02-05 15:03:25 +01:00
parent e7d63b9b0a
commit 9b9e7ec995
10 changed files with 1013 additions and 714 deletions

View file

@ -41,24 +41,29 @@ defmodule MvWeb.Components.FieldVisibilityDropdownComponent do
# RENDER
# ---------------------------------------------------------------------------
# Export-only alias; must not appear in dropdown (canonical UI key is membership_fee_status).
@payment_status_value "payment_status"
@impl true
def render(assigns) do
all_fields = assigns.all_fields || []
custom_fields = assigns.custom_fields || []
all_items =
Enum.map(extract_member_field_keys(all_fields), fn field ->
%{
value: field_to_string(field),
label: format_field_label(field)
}
end) ++
Enum.map(extract_custom_field_keys(all_fields), fn field ->
%{
value: field,
label: format_custom_field_label(field, custom_fields)
}
end)
(Enum.map(extract_member_field_keys(all_fields), fn field ->
%{
value: field_to_string(field),
label: format_field_label(field)
}
end) ++
Enum.map(extract_custom_field_keys(all_fields), fn field ->
%{
value: field,
label: format_custom_field_label(field, custom_fields)
}
end))
|> Enum.reject(fn item -> item.value == @payment_status_value end)
|> Enum.uniq_by(fn item -> item.value end)
assigns = assign(assigns, :all_items, all_items)

File diff suppressed because it is too large Load diff

View file

@ -293,6 +293,7 @@
</:col>
<:col
:let={member}
:if={:membership_fee_status in @member_fields_visible}
label={gettext("Membership Fee Status")}
>
<%= if badge = MvWeb.MemberLive.Index.MembershipFeeStatus.format_cycle_status_badge(

View file

@ -18,10 +18,25 @@ defmodule MvWeb.MemberLive.Index.FieldVisibility do
1. User-specific selection (from URL/Session/Cookie)
2. Global settings (from database)
3. Default (all fields visible)
## Pseudo Member Fields
Overview-only fields that are not in `Mv.Constants.member_fields()` (e.g. computed/UI-only).
They appear in the field dropdown and in `member_fields_visible` but are not domain attributes.
"""
alias Mv.Membership.Helpers.VisibilityConfig
# Single UI key for "Membership Fee Status"; only this appears in the dropdown.
@pseudo_member_fields [:membership_fee_status]
# Export/API may accept this as alias; must not appear in the UI options list.
@export_only_alias :payment_status
defp overview_member_fields do
Mv.Constants.member_fields() ++ @pseudo_member_fields
end
@doc """
Gets all available fields for selection.
@ -39,7 +54,10 @@ defmodule MvWeb.MemberLive.Index.FieldVisibility do
"""
@spec get_all_available_fields([struct()]) :: [atom() | String.t()]
def get_all_available_fields(custom_fields) do
member_fields = Mv.Constants.member_fields()
member_fields =
overview_member_fields()
|> Enum.reject(fn field -> field == @export_only_alias end)
custom_field_names = Enum.map(custom_fields, &"custom_field_#{&1.id}")
member_fields ++ custom_field_names
@ -115,6 +133,7 @@ defmodule MvWeb.MemberLive.Index.FieldVisibility do
field_selection
|> Enum.filter(fn {_field, visible} -> visible end)
|> Enum.map(fn {field_string, _visible} -> to_field_identifier(field_string) end)
|> Enum.uniq()
end
def get_visible_fields(_), do: []
@ -132,7 +151,7 @@ defmodule MvWeb.MemberLive.Index.FieldVisibility do
"""
@spec get_visible_member_fields(%{String.t() => boolean()}) :: [atom()]
def get_visible_member_fields(field_selection) when is_map(field_selection) do
member_fields = Mv.Constants.member_fields()
member_fields = overview_member_fields()
field_selection
|> Enum.filter(fn {field_string, visible} ->
@ -140,10 +159,61 @@ defmodule MvWeb.MemberLive.Index.FieldVisibility do
visible && field_atom in member_fields
end)
|> Enum.map(fn {field_string, _visible} -> to_field_identifier(field_string) end)
|> Enum.uniq()
end
def get_visible_member_fields(_), do: []
@doc """
Returns the list of computed (UI-only) member field atoms.
These fields are not in the database; they must not be used for Ash query
select/sort. Use this to filter sort options and validate sort_field.
"""
@spec computed_member_fields() :: [atom()]
def computed_member_fields, do: @pseudo_member_fields
@doc """
Visible member fields that are real DB attributes (from `Mv.Constants.member_fields()`).
Use for query select/sort. Not for rendering column visibility (use
`get_visible_member_fields/1` for that).
"""
@spec get_visible_member_fields_db(%{String.t() => boolean()}) :: [atom()]
def get_visible_member_fields_db(field_selection) when is_map(field_selection) do
db_fields = MapSet.new(Mv.Constants.member_fields())
field_selection
|> Enum.filter(fn {field_string, visible} ->
field_atom = to_field_identifier(field_string)
visible && field_atom in db_fields
end)
|> Enum.map(fn {field_string, _visible} -> to_field_identifier(field_string) end)
|> Enum.uniq()
end
def get_visible_member_fields_db(_), do: []
@doc """
Visible member fields that are computed/UI-only (e.g. membership_fee_status).
Use for rendering; do not use for query select or sort.
"""
@spec get_visible_member_fields_computed(%{String.t() => boolean()}) :: [atom()]
def get_visible_member_fields_computed(field_selection) when is_map(field_selection) do
computed_set = MapSet.new(@pseudo_member_fields)
field_selection
|> Enum.filter(fn {field_string, visible} ->
field_atom = to_field_identifier(field_string)
visible && field_atom in computed_set
end)
|> Enum.map(fn {field_string, _visible} -> to_field_identifier(field_string) end)
|> Enum.uniq()
end
def get_visible_member_fields_computed(_), do: []
@doc """
Gets visible custom fields from field selection.
@ -176,19 +246,23 @@ defmodule MvWeb.MemberLive.Index.FieldVisibility do
Map.merge(member_visibility, custom_field_visibility)
end
# Gets member field visibility from settings
# Gets member field visibility from settings (domain fields from settings, pseudo fields default true)
defp get_member_field_visibility_from_settings(settings) do
visibility_config =
VisibilityConfig.normalize(Map.get(settings, :member_field_visibility, %{}))
member_fields = Mv.Constants.member_fields()
domain_fields = Mv.Constants.member_fields()
Enum.reduce(member_fields, %{}, fn field, acc ->
field_string = Atom.to_string(field)
# exit_date defaults to false (hidden), all other fields default to true
default_visibility = if field == :exit_date, do: false, else: true
show_in_overview = Map.get(visibility_config, field, default_visibility)
Map.put(acc, field_string, show_in_overview)
domain_map =
Enum.reduce(domain_fields, %{}, fn field, acc ->
field_string = Atom.to_string(field)
default_visibility = if field == :exit_date, do: false, else: true
show_in_overview = Map.get(visibility_config, field, default_visibility)
Map.put(acc, field_string, show_in_overview)
end)
Enum.reduce(@pseudo_member_fields, domain_map, fn field, acc ->
Map.put(acc, Atom.to_string(field), true)
end)
end
@ -203,16 +277,20 @@ defmodule MvWeb.MemberLive.Index.FieldVisibility do
end)
end
# Converts field string to atom (for member fields) or keeps as string (for custom fields)
# Converts field string to atom (for member fields) or keeps as string (for custom fields).
# Maps export-only alias to canonical UI key so only one option controls the column.
defp to_field_identifier(field_string) when is_binary(field_string) do
if String.starts_with?(field_string, Mv.Constants.custom_field_prefix()) do
field_string
else
try do
String.to_existing_atom(field_string)
rescue
ArgumentError -> field_string
end
atom =
try do
String.to_existing_atom(field_string)
rescue
ArgumentError -> field_string
end
if atom == @export_only_alias, do: :membership_fee_status, else: atom
end
end