Compare commits
3 commits
f211f45cb2
...
d5df2338a7
| Author | SHA1 | Date | |
|---|---|---|---|
| d5df2338a7 | |||
| 1c8c5ae83b | |||
| 94bcb5dc8c |
7 changed files with 127 additions and 21 deletions
|
|
@ -433,6 +433,14 @@ defmodule Mv.Membership.MemberExport do
|
||||||
expand_field_with_computed(f, member_fields, computed_fields)
|
expand_field_with_computed(f, member_fields, computed_fields)
|
||||||
end)
|
end)
|
||||||
|
|
||||||
|
# If fee type is visible but start_date was not in the list, it won't be in db_with_insert
|
||||||
|
db_with_insert =
|
||||||
|
if "membership_fee_type" in member_fields and "membership_fee_type" not in db_with_insert do
|
||||||
|
db_with_insert ++ ["membership_fee_type"]
|
||||||
|
else
|
||||||
|
db_with_insert
|
||||||
|
end
|
||||||
|
|
||||||
remaining = Enum.reject(computed_fields, &(&1 in db_with_insert))
|
remaining = Enum.reject(computed_fields, &(&1 in db_with_insert))
|
||||||
db_with_insert ++ remaining
|
db_with_insert ++ remaining
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -133,7 +133,10 @@ defmodule Mv.Membership.MemberExport.Build do
|
||||||
"membership_fee_status" in parsed.member_fields
|
"membership_fee_status" in parsed.member_fields
|
||||||
|
|
||||||
need_groups = "groups" in parsed.member_fields
|
need_groups = "groups" in parsed.member_fields
|
||||||
need_membership_fee_type = "membership_fee_type" in parsed.member_fields
|
|
||||||
|
need_membership_fee_type =
|
||||||
|
"membership_fee_type" in parsed.member_fields or
|
||||||
|
parsed.sort_field == "membership_fee_type"
|
||||||
|
|
||||||
query =
|
query =
|
||||||
Member
|
Member
|
||||||
|
|
@ -199,10 +202,9 @@ defmodule Mv.Membership.MemberExport.Build do
|
||||||
field_atom = String.to_existing_atom(field)
|
field_atom = String.to_existing_atom(field)
|
||||||
|
|
||||||
if field_atom in Mv.Constants.member_fields() or field_atom == :membership_fee_type do
|
if field_atom in Mv.Constants.member_fields() or field_atom == :membership_fee_type do
|
||||||
sort_field =
|
key_fn = sort_key_fn_for_field(field_atom)
|
||||||
if field_atom == :membership_fee_type, do: :membership_fee_type_id, else: field_atom
|
compare_fn = build_compare_fn(order)
|
||||||
|
Enum.sort_by(members, key_fn, compare_fn)
|
||||||
sort_by_field(members, sort_field, order)
|
|
||||||
else
|
else
|
||||||
members
|
members
|
||||||
end
|
end
|
||||||
|
|
@ -212,13 +214,17 @@ defmodule Mv.Membership.MemberExport.Build do
|
||||||
|
|
||||||
defp sort_members_in_memory(members, _field, _order), do: members
|
defp sort_members_in_memory(members, _field, _order), do: members
|
||||||
|
|
||||||
defp sort_by_field(members, field_atom, order) do
|
defp sort_key_fn_for_field(:membership_fee_type) do
|
||||||
key_fn = fn member -> Map.get(member, field_atom) end
|
fn member ->
|
||||||
compare_fn = build_compare_fn(order)
|
case Map.get(member, :membership_fee_type) do
|
||||||
|
nil -> nil
|
||||||
Enum.sort_by(members, key_fn, compare_fn)
|
rel -> Map.get(rel, :name)
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
defp sort_key_fn_for_field(field_atom), do: fn member -> Map.get(member, field_atom) end
|
||||||
|
|
||||||
defp build_compare_fn("asc"), do: fn a, b -> a <= b end
|
defp build_compare_fn("asc"), do: fn a, b -> a <= b end
|
||||||
defp build_compare_fn("desc"), do: fn a, b -> b <= a end
|
defp build_compare_fn("desc"), do: fn a, b -> b <= a end
|
||||||
defp build_compare_fn(_), do: fn _a, _b -> true end
|
defp build_compare_fn(_), do: fn _a, _b -> true end
|
||||||
|
|
@ -261,7 +267,7 @@ defmodule Mv.Membership.MemberExport.Build do
|
||||||
|
|
||||||
defp apply_fee_type_sort(query, order) do
|
defp apply_fee_type_sort(query, order) do
|
||||||
order_atom = if order == "desc", do: :desc, else: :asc
|
order_atom = if order == "desc", do: :desc, else: :asc
|
||||||
{Ash.Query.sort(query, membership_fee_type_id: order_atom), false}
|
{Ash.Query.sort(query, [{"membership_fee_type.name", order_atom}]), false}
|
||||||
end
|
end
|
||||||
|
|
||||||
defp apply_standard_member_sort(query, field, order) do
|
defp apply_standard_member_sort(query, field, order) do
|
||||||
|
|
@ -275,9 +281,11 @@ defmodule Mv.Membership.MemberExport.Build do
|
||||||
order_atom = if order == "desc", do: :desc, else: :asc
|
order_atom = if order == "desc", do: :desc, else: :asc
|
||||||
|
|
||||||
sort_field =
|
sort_field =
|
||||||
if field_atom == :membership_fee_type, do: :membership_fee_type_id, else: field_atom
|
if field_atom == :membership_fee_type,
|
||||||
|
do: {"membership_fee_type.name", order_atom},
|
||||||
|
else: {field_atom, order_atom}
|
||||||
|
|
||||||
{Ash.Query.sort(query, [{sort_field, order_atom}]), false}
|
{Ash.Query.sort(query, [sort_field]), false}
|
||||||
else
|
else
|
||||||
{query, false}
|
{query, false}
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -238,7 +238,10 @@ defmodule MvWeb.MemberExportController do
|
||||||
parsed.computed_fields != [] and "membership_fee_status" in parsed.computed_fields
|
parsed.computed_fields != [] and "membership_fee_status" in parsed.computed_fields
|
||||||
|
|
||||||
need_groups = "groups" in parsed.member_fields
|
need_groups = "groups" in parsed.member_fields
|
||||||
need_membership_fee_type = "membership_fee_type" in parsed.member_fields
|
|
||||||
|
need_membership_fee_type =
|
||||||
|
"membership_fee_type" in parsed.member_fields or
|
||||||
|
parsed.sort_field == "membership_fee_type"
|
||||||
|
|
||||||
query =
|
query =
|
||||||
Member
|
Member
|
||||||
|
|
@ -368,7 +371,7 @@ defmodule MvWeb.MemberExportController do
|
||||||
|
|
||||||
defp apply_membership_fee_type_sort_export(query, order) do
|
defp apply_membership_fee_type_sort_export(query, order) do
|
||||||
order_atom = if order == "desc", do: :desc, else: :asc
|
order_atom = if order == "desc", do: :desc, else: :asc
|
||||||
{Ash.Query.sort(query, membership_fee_type_id: order_atom), false}
|
{Ash.Query.sort(query, [{"membership_fee_type.name", order_atom}]), false}
|
||||||
end
|
end
|
||||||
|
|
||||||
defp apply_member_field_sort_export(query, field, order) do
|
defp apply_member_field_sort_export(query, field, order) do
|
||||||
|
|
|
||||||
|
|
@ -965,9 +965,10 @@ defmodule MvWeb.MemberLive.Index do
|
||||||
query =
|
query =
|
||||||
Ash.Query.load(query, groups: [:id, :name, :slug])
|
Ash.Query.load(query, groups: [:id, :name, :slug])
|
||||||
|
|
||||||
# Load membership_fee_type when the column is visible
|
# Load membership_fee_type when the column is visible or when sorting by it
|
||||||
query =
|
query =
|
||||||
if :membership_fee_type in socket.assigns.member_fields_visible do
|
if :membership_fee_type in socket.assigns.member_fields_visible or
|
||||||
|
socket.assigns.sort_field in [:membership_fee_type, "membership_fee_type"] do
|
||||||
Ash.Query.load(query, membership_fee_type: [:id, :name])
|
Ash.Query.load(query, membership_fee_type: [:id, :name])
|
||||||
else
|
else
|
||||||
query
|
query
|
||||||
|
|
@ -1133,9 +1134,9 @@ defmodule MvWeb.MemberLive.Index do
|
||||||
field in [:groups, "groups"] ->
|
field in [:groups, "groups"] ->
|
||||||
{query, true}
|
{query, true}
|
||||||
|
|
||||||
# Membership fee type sort -> by FK at DB
|
# Membership fee type sort -> by related name at DB
|
||||||
field in [:membership_fee_type, "membership_fee_type"] ->
|
field in [:membership_fee_type, "membership_fee_type"] ->
|
||||||
{Ash.Query.sort(query, membership_fee_type_id: order), false}
|
{Ash.Query.sort(query, [{"membership_fee_type.name", order}]), false}
|
||||||
|
|
||||||
# Custom field sort -> after load
|
# Custom field sort -> after load
|
||||||
custom_field_sort?(field) ->
|
custom_field_sort?(field) ->
|
||||||
|
|
@ -1777,6 +1778,15 @@ defmodule MvWeb.MemberLive.Index do
|
||||||
end
|
end
|
||||||
end)
|
end)
|
||||||
|
|
||||||
|
# If fee type is visible but start_date was not in the list, append it
|
||||||
|
with_extras =
|
||||||
|
if :membership_fee_type in (member_fields_visible || []) and
|
||||||
|
:membership_fee_type not in with_extras do
|
||||||
|
with_extras ++ [:membership_fee_type]
|
||||||
|
else
|
||||||
|
with_extras
|
||||||
|
end
|
||||||
|
|
||||||
if :groups in (member_fields_visible || []), do: with_extras ++ [:groups], else: with_extras
|
if :groups in (member_fields_visible || []), do: with_extras ++ [:groups], else: with_extras
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -1815,6 +1825,14 @@ defmodule MvWeb.MemberLive.Index do
|
||||||
&expand_db_string_for_export(&1, membership_fee_type_visible, computed_strings)
|
&expand_db_string_for_export(&1, membership_fee_type_visible, computed_strings)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# If fee type is visible but start_date was not in the list, append it before computed/groups
|
||||||
|
db_with_extras =
|
||||||
|
if membership_fee_type_visible and "membership_fee_type" not in db_with_extras do
|
||||||
|
db_with_extras ++ ["membership_fee_type"]
|
||||||
|
else
|
||||||
|
db_with_extras
|
||||||
|
end
|
||||||
|
|
||||||
# Any remaining computed fields not inserted above (future-proof)
|
# Any remaining computed fields not inserted above (future-proof)
|
||||||
remaining_computed =
|
remaining_computed =
|
||||||
computed_strings
|
computed_strings
|
||||||
|
|
|
||||||
|
|
@ -2917,6 +2917,6 @@ msgstr "Für die Vereinfacht-Integration erforderlich und kann nicht deaktiviert
|
||||||
|
|
||||||
#: lib/mv_web/live/member_live/index.html.heex
|
#: lib/mv_web/live/member_live/index.html.heex
|
||||||
#: lib/mv_web/translations/member_fields.ex
|
#: lib/mv_web/translations/member_fields.ex
|
||||||
#, elixir-autogen, elixir-format, fuzzy
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Fee Type"
|
msgid "Fee Type"
|
||||||
msgstr "Beitragsart"
|
msgstr "Beitragsart"
|
||||||
|
|
|
||||||
|
|
@ -2917,6 +2917,6 @@ msgstr "Required for Vereinfacht integration and cannot be disabled."
|
||||||
|
|
||||||
#: lib/mv_web/live/member_live/index.html.heex
|
#: lib/mv_web/live/member_live/index.html.heex
|
||||||
#: lib/mv_web/translations/member_fields.ex
|
#: lib/mv_web/translations/member_fields.ex
|
||||||
#, elixir-autogen, elixir-format, fuzzy
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Fee Type"
|
msgid "Fee Type"
|
||||||
msgstr "Fee Type"
|
msgstr "Fee Type"
|
||||||
|
|
|
||||||
|
|
@ -177,6 +177,39 @@ defmodule MvWeb.MemberExportControllerTest do
|
||||||
assert body =~ "Alice"
|
assert body =~ "Alice"
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Regression: when membership_fee_start_date is not in member_fields, Fee Type must still be exported (append fallback)
|
||||||
|
test "export includes Fee Type when only first_name and membership_fee_type are requested (no start_date)",
|
||||||
|
%{
|
||||||
|
conn: conn,
|
||||||
|
member1: m1
|
||||||
|
} do
|
||||||
|
payload = %{
|
||||||
|
"selected_ids" => [m1.id],
|
||||||
|
"member_fields" => ["first_name", "membership_fee_type"],
|
||||||
|
"custom_field_ids" => [],
|
||||||
|
"query" => nil,
|
||||||
|
"sort_field" => nil,
|
||||||
|
"sort_order" => nil
|
||||||
|
}
|
||||||
|
|
||||||
|
conn = get(conn, "/members")
|
||||||
|
csrf_token = csrf_token_from_conn(conn)
|
||||||
|
|
||||||
|
conn =
|
||||||
|
post(conn, "/members/export.csv", %{
|
||||||
|
"payload" => Jason.encode!(payload),
|
||||||
|
"_csrf_token" => csrf_token
|
||||||
|
})
|
||||||
|
|
||||||
|
assert conn.status == 200
|
||||||
|
body = response(conn, 200)
|
||||||
|
header = body |> export_lines() |> hd()
|
||||||
|
|
||||||
|
assert header =~ "Fee Type"
|
||||||
|
assert header =~ "First Name"
|
||||||
|
assert body =~ "Alice"
|
||||||
|
end
|
||||||
|
|
||||||
test "export includes membership_fee_status computed field when requested", %{
|
test "export includes membership_fee_status computed field when requested", %{
|
||||||
conn: conn,
|
conn: conn,
|
||||||
member1: m1
|
member1: m1
|
||||||
|
|
@ -532,4 +565,40 @@ defmodule MvWeb.MemberExportControllerTest do
|
||||||
assert membership_idx < active_idx
|
assert membership_idx < active_idx
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe "POST /members/export.pdf" do
|
||||||
|
test "PDF export includes Fee Type column when requested without membership_fee_start_date",
|
||||||
|
%{
|
||||||
|
conn: conn
|
||||||
|
} do
|
||||||
|
m =
|
||||||
|
Fixtures.member_fixture(%{first_name: "PDF", last_name: "Test", email: "pdf@example.com"})
|
||||||
|
|
||||||
|
payload = %{
|
||||||
|
"selected_ids" => [m.id],
|
||||||
|
"member_fields" => ["first_name", "membership_fee_type"],
|
||||||
|
"custom_field_ids" => [],
|
||||||
|
"query" => nil,
|
||||||
|
"sort_field" => nil,
|
||||||
|
"sort_order" => nil
|
||||||
|
}
|
||||||
|
|
||||||
|
conn = get(conn, "/members")
|
||||||
|
csrf_token = csrf_token_from_conn(conn)
|
||||||
|
|
||||||
|
conn =
|
||||||
|
post(conn, "/members/export.pdf", %{
|
||||||
|
"payload" => Jason.encode!(payload),
|
||||||
|
"_csrf_token" => csrf_token
|
||||||
|
})
|
||||||
|
|
||||||
|
assert conn.status == 200
|
||||||
|
assert get_resp_header(conn, "content-type") |> List.first() =~ "application/pdf"
|
||||||
|
|
||||||
|
body = response(conn, 200)
|
||||||
|
|
||||||
|
# PDF is generated successfully with Fee Type in columns (regression: fee type without start_date)
|
||||||
|
assert is_binary(body) and byte_size(body) > 0
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue