fix: sorting and filter for export
This commit is contained in:
parent
e7d63b9b0a
commit
9b9e7ec995
10 changed files with 1013 additions and 714 deletions
|
|
@ -61,6 +61,7 @@ defmodule MvWeb.MemberExportController do
|
|||
%{
|
||||
selected_ids: filter_valid_uuids(extract_list(params, "selected_ids")),
|
||||
member_fields: filter_allowed_member_fields(extract_list(params, "member_fields")),
|
||||
computed_fields: filter_existing_atoms(extract_list(params, "computed_fields")),
|
||||
custom_field_ids: filter_valid_uuids(extract_list(params, "custom_field_ids")),
|
||||
query: extract_string(params, "query"),
|
||||
sort_field: extract_string(params, "sort_field"),
|
||||
|
|
@ -68,6 +69,20 @@ defmodule MvWeb.MemberExportController do
|
|||
}
|
||||
end
|
||||
|
||||
defp filter_existing_atoms(list) when is_list(list) do
|
||||
list
|
||||
|> Enum.filter(&is_binary/1)
|
||||
|> Enum.filter(fn name ->
|
||||
try do
|
||||
_ = String.to_existing_atom(name)
|
||||
true
|
||||
rescue
|
||||
ArgumentError -> false
|
||||
end
|
||||
end)
|
||||
|> Enum.uniq()
|
||||
end
|
||||
|
||||
defp extract_list(params, key) do
|
||||
case Map.get(params, key) do
|
||||
list when is_list(list) -> list
|
||||
|
|
@ -107,9 +122,16 @@ defmodule MvWeb.MemberExportController do
|
|||
end
|
||||
|
||||
defp run_export(conn, actor, parsed) do
|
||||
# FIX: Wenn nach einem Custom Field sortiert wird, muss dieses Feld geladen werden,
|
||||
# auch wenn es nicht exportiert wird (sonst kann Export nicht korrekt sortieren).
|
||||
parsed =
|
||||
parsed
|
||||
|> ensure_sort_custom_field_loaded()
|
||||
|
||||
with {:ok, custom_fields_by_id} <- load_custom_fields_by_id(parsed.custom_field_ids, actor),
|
||||
{:ok, members} <- load_members_for_export(actor, parsed, custom_fields_by_id) do
|
||||
csv_iodata = MembersCSV.export(members, parsed.member_fields, custom_fields_by_id)
|
||||
columns = build_columns(conn, parsed, custom_fields_by_id)
|
||||
csv_iodata = MembersCSV.export(members, columns)
|
||||
filename = "members-#{Date.utc_today()}.csv"
|
||||
|
||||
send_download(
|
||||
|
|
@ -124,6 +146,26 @@ defmodule MvWeb.MemberExportController do
|
|||
end
|
||||
end
|
||||
|
||||
defp ensure_sort_custom_field_loaded(%{custom_field_ids: ids, sort_field: sort_field} = parsed) do
|
||||
case extract_sort_custom_field_id(sort_field) do
|
||||
nil ->
|
||||
parsed
|
||||
|
||||
id ->
|
||||
%{parsed | custom_field_ids: Enum.uniq([id | ids])}
|
||||
end
|
||||
end
|
||||
|
||||
defp extract_sort_custom_field_id(field) when is_binary(field) do
|
||||
if String.starts_with?(field, @custom_field_prefix) do
|
||||
String.trim_leading(field, @custom_field_prefix)
|
||||
else
|
||||
nil
|
||||
end
|
||||
end
|
||||
|
||||
defp extract_sort_custom_field_id(_), do: nil
|
||||
|
||||
defp load_custom_fields_by_id([], _actor), do: {:ok, %{}}
|
||||
|
||||
defp load_custom_fields_by_id(custom_field_ids, actor) do
|
||||
|
|
@ -148,17 +190,13 @@ defmodule MvWeb.MemberExportController do
|
|||
|
||||
defp build_custom_fields_by_id(custom_field_ids, custom_fields) do
|
||||
Enum.reduce(custom_field_ids, %{}, fn id, acc ->
|
||||
find_and_add_custom_field(acc, id, custom_fields)
|
||||
case Enum.find(custom_fields, fn cf -> to_string(cf.id) == to_string(id) end) do
|
||||
nil -> acc
|
||||
cf -> Map.put(acc, id, cf)
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
defp find_and_add_custom_field(acc, id, custom_fields) do
|
||||
case Enum.find(custom_fields, fn cf -> to_string(cf.id) == to_string(id) end) do
|
||||
nil -> acc
|
||||
cf -> Map.put(acc, id, cf)
|
||||
end
|
||||
end
|
||||
|
||||
defp load_members_for_export(actor, parsed, custom_fields_by_id) do
|
||||
select_fields = [:id] ++ Enum.map(parsed.member_fields, &String.to_existing_atom/1)
|
||||
|
||||
|
|
@ -170,20 +208,20 @@ defmodule MvWeb.MemberExportController do
|
|||
|
||||
query =
|
||||
if parsed.selected_ids != [] do
|
||||
# selected export: filtert die Menge, aber die Sortierung muss trotzdem wie in der Tabelle angewandt werden
|
||||
Ash.Query.filter(query, expr(id in ^parsed.selected_ids))
|
||||
else
|
||||
query
|
||||
|> apply_search_export(parsed.query)
|
||||
|> then(fn q ->
|
||||
{q, _sort_after_load} = maybe_sort_export(q, parsed.sort_field, parsed.sort_order)
|
||||
q
|
||||
end)
|
||||
end
|
||||
|
||||
# FIX: Sortierung IMMER anwenden (auch bei selected_ids)
|
||||
{query, sort_after_load} = maybe_sort_export(query, parsed.sort_field, parsed.sort_order)
|
||||
|
||||
case Ash.read(query, actor: actor) do
|
||||
{:ok, members} ->
|
||||
members =
|
||||
if parsed.selected_ids == [] and sort_after_load?(parsed.sort_field) do
|
||||
if sort_after_load do
|
||||
sort_members_by_custom_field_export(
|
||||
members,
|
||||
parsed.sort_field,
|
||||
|
|
@ -191,7 +229,6 @@ defmodule MvWeb.MemberExportController do
|
|||
Map.values(custom_fields_by_id)
|
||||
)
|
||||
else
|
||||
# selected_ids != []: no sort. selected_ids == [] and DB sort: already in query.
|
||||
members
|
||||
end
|
||||
|
||||
|
|
@ -228,25 +265,29 @@ defmodule MvWeb.MemberExportController do
|
|||
defp maybe_sort_export(query, _field, nil), do: {query, false}
|
||||
|
||||
defp maybe_sort_export(query, field, order) when is_binary(field) do
|
||||
if custom_field_sort?(field) do
|
||||
{query, true}
|
||||
else
|
||||
field_atom = String.to_existing_atom(field)
|
||||
cond do
|
||||
custom_field_sort?(field) ->
|
||||
# Custom field sort → in-memory nach dem Read (wie Tabelle)
|
||||
{query, true}
|
||||
|
||||
if field_atom in (Mv.Constants.member_fields() -- [:notes]) do
|
||||
{Ash.Query.sort(query, [{field_atom, String.to_existing_atom(order)}]), false}
|
||||
else
|
||||
{query, false}
|
||||
end
|
||||
true ->
|
||||
field_atom = String.to_existing_atom(field)
|
||||
|
||||
if field_atom in (Mv.Constants.member_fields() -- [:notes]) do
|
||||
{Ash.Query.sort(query, [{field_atom, String.to_existing_atom(order)}]), false}
|
||||
else
|
||||
{query, false}
|
||||
end
|
||||
end
|
||||
rescue
|
||||
ArgumentError -> {query, false}
|
||||
end
|
||||
|
||||
defp sort_after_load?(field) when is_binary(field),
|
||||
do: String.starts_with?(field, @custom_field_prefix)
|
||||
defp custom_field_sort?(field), do: String.starts_with?(field, @custom_field_prefix)
|
||||
|
||||
defp sort_after_load?(_), do: false
|
||||
# ------------------------------------------------------------------
|
||||
# Custom field sorting (match member table behavior)
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
defp sort_members_by_custom_field_export(members, _field, _order, _custom_fields)
|
||||
when members == [],
|
||||
|
|
@ -254,26 +295,60 @@ defmodule MvWeb.MemberExportController do
|
|||
|
||||
defp sort_members_by_custom_field_export(members, field, order, custom_fields)
|
||||
when is_binary(field) do
|
||||
order = order || "asc"
|
||||
id_str = String.trim_leading(field, @custom_field_prefix)
|
||||
custom_field = Enum.find(custom_fields, fn cf -> to_string(cf.id) == id_str end)
|
||||
if is_nil(custom_field), do: members
|
||||
|
||||
extract_sort_val = fn member ->
|
||||
cfv = find_cfv(member, custom_field)
|
||||
if cfv, do: extract_sort_value(cfv.value, custom_field.value_type), else: nil
|
||||
end
|
||||
custom_field =
|
||||
Enum.find(custom_fields, fn cf -> to_string(cf.id) == id_str end)
|
||||
|
||||
sorted =
|
||||
if is_nil(custom_field) do
|
||||
members
|
||||
|> Enum.sort_by(extract_sort_val, fn
|
||||
nil, _ -> false
|
||||
_, nil -> true
|
||||
a, b -> if order == "desc", do: a >= b, else: a <= b
|
||||
end)
|
||||
else
|
||||
# Match table:
|
||||
# 1) values first, empty last
|
||||
# 2) sort only values
|
||||
# 3) for desc, reverse only the values-part
|
||||
{with_values, without_values} =
|
||||
Enum.split_with(members, fn member ->
|
||||
has_non_empty_custom_field_value?(member, custom_field)
|
||||
end)
|
||||
|
||||
if order == "desc", do: Enum.reverse(sorted), else: sorted
|
||||
sorted_with_values =
|
||||
Enum.sort_by(with_values, fn member ->
|
||||
member
|
||||
|> find_cfv(custom_field)
|
||||
|> case do
|
||||
nil -> nil
|
||||
cfv -> extract_sort_value(cfv.value, custom_field.value_type)
|
||||
end
|
||||
end)
|
||||
|
||||
sorted_with_values =
|
||||
if order == "desc", do: Enum.reverse(sorted_with_values), else: sorted_with_values
|
||||
|
||||
sorted_with_values ++ without_values
|
||||
end
|
||||
end
|
||||
|
||||
defp has_non_empty_custom_field_value?(member, custom_field) do
|
||||
case find_cfv(member, custom_field) do
|
||||
nil ->
|
||||
false
|
||||
|
||||
cfv ->
|
||||
extracted = extract_sort_value(cfv.value, custom_field.value_type)
|
||||
not empty_value?(extracted, custom_field.value_type)
|
||||
end
|
||||
end
|
||||
|
||||
defp empty_value?(nil, _type), do: true
|
||||
|
||||
defp empty_value?(value, type) when type in [:string, :email] and is_binary(value) do
|
||||
String.trim(value) == ""
|
||||
end
|
||||
|
||||
defp empty_value?(_value, _type), do: false
|
||||
|
||||
defp find_cfv(member, custom_field) do
|
||||
(member.custom_field_values || [])
|
||||
|> Enum.find(fn cfv ->
|
||||
|
|
@ -283,15 +358,76 @@ defmodule MvWeb.MemberExportController do
|
|||
end)
|
||||
end
|
||||
|
||||
defp build_columns(conn, parsed, custom_fields_by_id) do
|
||||
member_cols =
|
||||
Enum.map(parsed.member_fields, fn field ->
|
||||
%{
|
||||
header: member_field_header(conn, field),
|
||||
kind: :member_field,
|
||||
key: field
|
||||
}
|
||||
end)
|
||||
|
||||
computed_cols =
|
||||
Enum.map(parsed.computed_fields, fn key ->
|
||||
%{
|
||||
header: computed_field_header(conn, key),
|
||||
kind: :computed,
|
||||
key: key
|
||||
}
|
||||
end)
|
||||
|
||||
custom_cols =
|
||||
parsed.custom_field_ids
|
||||
|> Enum.map(fn id ->
|
||||
cf = Map.get(custom_fields_by_id, id) || Map.get(custom_fields_by_id, to_string(id))
|
||||
|
||||
if cf do
|
||||
%{
|
||||
header: custom_field_header(conn, cf),
|
||||
kind: :custom_field,
|
||||
key: to_string(id),
|
||||
custom_field: cf
|
||||
}
|
||||
else
|
||||
nil
|
||||
end
|
||||
end)
|
||||
|> Enum.reject(&is_nil/1)
|
||||
|
||||
member_cols ++ computed_cols ++ custom_cols
|
||||
end
|
||||
|
||||
# --- headers: hier solltest du idealerweise eure bestehenden "display name" Helfer verwenden ---
|
||||
defp member_field_header(_conn, field) when is_binary(field) do
|
||||
# TODO: hier euren bestehenden display-name helper verwenden (wie Tabelle)
|
||||
humanize_field(field)
|
||||
end
|
||||
|
||||
defp computed_field_header(_conn, key) when is_binary(key) do
|
||||
# TODO: display-name helper für computed fields verwenden
|
||||
humanize_field(key)
|
||||
end
|
||||
|
||||
defp custom_field_header(_conn, cf) do
|
||||
# Custom fields: meist ist cf.name bereits der Display Name
|
||||
cf.name
|
||||
end
|
||||
|
||||
defp humanize_field(str) do
|
||||
str
|
||||
|> String.replace("_", " ")
|
||||
|> String.capitalize()
|
||||
end
|
||||
|
||||
defp extract_sort_value(%Ash.Union{value: value, type: type}, _),
|
||||
do: extract_sort_value(value, type)
|
||||
|
||||
defp extract_sort_value(nil, _), do: nil
|
||||
defp extract_sort_value(value, :string) when is_binary(value), do: value
|
||||
defp extract_sort_value(value, :integer) when is_integer(value), do: value
|
||||
defp extract_sort_value(value, :boolean) when is_boolean(value), do: value
|
||||
defp extract_sort_value(%Date{} = d, :date), do: d
|
||||
defp extract_sort_value(value, :email) when is_binary(value), do: value
|
||||
defp extract_sort_value(value, _), do: to_string(value)
|
||||
|
||||
defp custom_field_sort?(field), do: String.starts_with?(field, @custom_field_prefix)
|
||||
end
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue