diff --git a/lib/mv/membership/member_export.ex b/lib/mv/membership/member_export.ex index b4272b0..a017480 100644 --- a/lib/mv/membership/member_export.ex +++ b/lib/mv/membership/member_export.ex @@ -16,7 +16,7 @@ defmodule Mv.Membership.MemberExport do alias MvWeb.MemberLive.Index.MembershipFeeStatus @member_fields_allowlist (Mv.Constants.member_fields() |> Enum.map(&Atom.to_string/1)) ++ - ["membership_fee_status", "groups"] + ["membership_fee_type", "membership_fee_status", "groups"] @computed_export_fields ["membership_fee_status"] @computed_insert_after "membership_fee_start_date" @custom_field_prefix Mv.Constants.custom_field_prefix() @@ -326,10 +326,10 @@ defmodule Mv.Membership.MemberExport do # Separate groups from other fields (groups is handled as a special field, not a member field) groups_field = if "groups" in member_fields, do: ["groups"], else: [] - # final member_fields list (used for column specs order): table order + computed inserted + groups + # final member_fields list (used for column specs order): table order + fee type + computed + groups ordered_member_fields = selectable_member_fields - |> insert_computed_fields_like_table(computed_fields) + |> insert_fee_type_and_computed_fields_like_table(computed_fields, member_fields) |> then(fn fields -> fields ++ groups_field end) %{ @@ -420,27 +420,44 @@ defmodule Mv.Membership.MemberExport do table_order |> Enum.filter(&(&1 in fields)) end - defp insert_computed_fields_like_table(db_fields_ordered, computed_fields) do - # Insert membership_fee_status right after membership_fee_start_date (if both selected), - # otherwise append at the end of DB fields. + defp insert_fee_type_and_computed_fields_like_table( + db_fields_ordered, + computed_fields, + member_fields + ) do computed_fields = computed_fields || [] + member_fields = member_fields || [] db_with_insert = Enum.flat_map(db_fields_ordered, fn f -> - if f == @computed_insert_after and "membership_fee_status" in computed_fields do - [f, "membership_fee_status"] - else - [f] - end + expand_field_with_computed(f, member_fields, computed_fields) end) - remaining = - computed_fields - |> Enum.reject(&(&1 in db_with_insert)) - + remaining = Enum.reject(computed_fields, &(&1 in db_with_insert)) db_with_insert ++ remaining end + # Insert membership_fee_type and membership_fee_status after membership_fee_start_date (table order). + defp expand_field_with_computed(f, member_fields, computed_fields) do + if f == @computed_insert_after do + extra = [] + + extra = + if "membership_fee_type" in member_fields, + do: extra ++ ["membership_fee_type"], + else: extra + + extra = + if "membership_fee_status" in computed_fields, + do: extra ++ ["membership_fee_status"], + else: extra + + [f] ++ extra + else + [f] + end + end + defp normalize_computed_fields(fields) when is_list(fields) do fields |> Enum.filter(&is_binary/1) diff --git a/lib/mv/membership/member_export/build.ex b/lib/mv/membership/member_export/build.ex index 9e0cc7b..8a5aa60 100644 --- a/lib/mv/membership/member_export/build.ex +++ b/lib/mv/membership/member_export/build.ex @@ -133,6 +133,7 @@ defmodule Mv.Membership.MemberExport.Build do "membership_fee_status" in parsed.member_fields need_groups = "groups" in parsed.member_fields + need_membership_fee_type = "membership_fee_type" in parsed.member_fields query = Member @@ -141,6 +142,7 @@ defmodule Mv.Membership.MemberExport.Build do |> load_custom_field_values_query(custom_field_ids_union) |> maybe_load_cycles(need_cycles, parsed.show_current_cycle) |> maybe_load_groups(need_groups) + |> maybe_load_membership_fee_type(need_membership_fee_type) query = if parsed.selected_ids != [] do @@ -196,8 +198,11 @@ defmodule Mv.Membership.MemberExport.Build do defp sort_members_in_memory(members, field, order) when is_binary(field) do field_atom = String.to_existing_atom(field) - if field_atom in Mv.Constants.member_fields() do - sort_by_field(members, field_atom, order) + if field_atom in Mv.Constants.member_fields() or field_atom == :membership_fee_type do + sort_field = + if field_atom == :membership_fee_type, do: :membership_fee_type_id, else: field_atom + + sort_by_field(members, sort_field, order) else members end @@ -245,26 +250,39 @@ defmodule Mv.Membership.MemberExport.Build do defp maybe_sort(query, field, order) when is_binary(field) do cond do - field == "groups" -> - # Groups sort → in-memory nach dem Read (wie Tabelle) - {query, true} - - custom_field_sort?(field) -> - {query, true} - - 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 + field == "groups" -> {query, true} + field == "membership_fee_type" -> apply_fee_type_sort(query, order) + custom_field_sort?(field) -> {query, true} + true -> apply_standard_member_sort(query, field, order) end rescue ArgumentError -> {query, false} end + defp apply_fee_type_sort(query, order) do + order_atom = if order == "desc", do: :desc, else: :asc + {Ash.Query.sort(query, membership_fee_type_id: order_atom), false} + end + + defp apply_standard_member_sort(query, field, order) do + field_atom = String.to_existing_atom(field) + + sortable = + field_atom in (Mv.Constants.member_fields() -- [:notes]) or + field_atom == :membership_fee_type + + if sortable do + order_atom = if order == "desc", do: :desc, else: :asc + + sort_field = + if field_atom == :membership_fee_type, do: :membership_fee_type_id, else: field_atom + + {Ash.Query.sort(query, [{sort_field, order_atom}]), false} + else + {query, false} + end + end + defp sort_members_by_custom_field(members, _field, _order, _custom_fields) when members == [], do: [] @@ -344,6 +362,12 @@ defmodule Mv.Membership.MemberExport.Build do Ash.Query.load(query, groups: [:id, :name]) end + defp maybe_load_membership_fee_type(query, false), do: query + + defp maybe_load_membership_fee_type(query, true) do + Ash.Query.load(query, membership_fee_type: [:id, :name]) + end + defp apply_cycle_status_filter(members, nil, _show_current), do: members defp apply_cycle_status_filter(members, status, show_current) when status in [:paid, :unpaid] do @@ -393,6 +417,19 @@ defmodule Mv.Membership.MemberExport.Build do } end) + membership_fee_type_col = + if "membership_fee_type" in parsed.member_fields do + [ + %{ + key: :membership_fee_type, + kind: :membership_fee_type, + label: label_fn.(:membership_fee_type) + } + ] + else + [] + end + groups_col = if "groups" in parsed.member_fields do [ @@ -424,7 +461,8 @@ defmodule Mv.Membership.MemberExport.Build do end) |> Enum.reject(&is_nil/1) - member_cols ++ computed_cols ++ groups_col ++ custom_cols + # Table order: ... membership_fee_start_date, membership_fee_type, membership_fee_status, groups, custom + member_cols ++ membership_fee_type_col ++ computed_cols ++ groups_col ++ custom_cols end defp build_rows(members, columns, custom_fields_by_id) do @@ -454,6 +492,17 @@ defmodule Mv.Membership.MemberExport.Build do if is_binary(value), do: value, else: "" end + defp cell_value( + member, + %{kind: :membership_fee_type, key: :membership_fee_type}, + _custom_fields_by_id + ) do + case Map.get(member, :membership_fee_type) do + %{name: name} when is_binary(name) -> name + _ -> "" + end + end + defp cell_value(member, %{kind: :groups, key: :groups}, _custom_fields_by_id) do groups = Map.get(member, :groups) || [] format_groups(groups) diff --git a/lib/mv_web/controllers/member_pdf_export_controller.ex b/lib/mv_web/controllers/member_pdf_export_controller.ex index 63feef2..f00c0d1 100644 --- a/lib/mv_web/controllers/member_pdf_export_controller.ex +++ b/lib/mv_web/controllers/member_pdf_export_controller.ex @@ -20,7 +20,8 @@ defmodule MvWeb.MemberPdfExportController do @invalid_json_message "invalid JSON" @export_failed_message "Failed to generate PDF export" - @allowed_member_field_strings Mv.Constants.member_fields() |> Enum.map(&Atom.to_string/1) + @allowed_member_field_strings (Mv.Constants.member_fields() |> Enum.map(&Atom.to_string/1)) ++ + ["membership_fee_type", "groups"] def export(conn, %{"payload" => payload}) when is_binary(payload) do actor = current_actor(conn)