From f3b213ececa7eb3471fb1f7275a5681f2ae1e044 Mon Sep 17 00:00:00 2001 From: Moritz Date: Mon, 23 Feb 2026 23:53:51 +0100 Subject: [PATCH] feat(export): include Fee Type in CSV export Payload and column_order when visible; allowlist, load, sort; MembersCSV cell for :membership_fee_type. --- lib/mv/membership/members_csv.ex | 7 ++ .../controllers/member_export_controller.ex | 67 ++++++++++++++++--- 2 files changed, 63 insertions(+), 11 deletions(-) diff --git a/lib/mv/membership/members_csv.ex b/lib/mv/membership/members_csv.ex index a47af8d..3d1fdd8 100644 --- a/lib/mv/membership/members_csv.ex +++ b/lib/mv/membership/members_csv.ex @@ -59,6 +59,13 @@ defmodule Mv.Membership.MembersCSV do if is_binary(value), do: value, else: "" end + defp cell_value(member, %{kind: :membership_fee_type, key: :membership_fee_type}) 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}) do groups = Map.get(member, :groups) || [] format_groups(groups) diff --git a/lib/mv_web/controllers/member_export_controller.ex b/lib/mv_web/controllers/member_export_controller.ex index 08bcba7..b5386a9 100644 --- a/lib/mv_web/controllers/member_export_controller.ex +++ b/lib/mv_web/controllers/member_export_controller.ex @@ -19,7 +19,7 @@ defmodule MvWeb.MemberExportController do use Gettext, backend: MvWeb.Gettext @member_fields_allowlist (Mv.Constants.member_fields() |> Enum.map(&Atom.to_string/1)) ++ - ["groups"] + ["membership_fee_type", "groups"] @computed_export_fields ["membership_fee_status"] @custom_field_prefix Mv.Constants.custom_field_prefix() @@ -238,6 +238,7 @@ defmodule MvWeb.MemberExportController do parsed.computed_fields != [] and "membership_fee_status" in parsed.computed_fields need_groups = "groups" in parsed.member_fields + need_membership_fee_type = "membership_fee_type" in parsed.member_fields query = Member @@ -246,6 +247,7 @@ defmodule MvWeb.MemberExportController do |> load_custom_field_values_query(parsed.custom_field_ids) |> 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 @@ -296,6 +298,12 @@ defmodule MvWeb.MemberExportController 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 + # Adds computed field values to members (e.g. membership_fee_status) defp add_computed_fields(members, computed_fields, show_current_cycle) do if "membership_fee_status" in computed_fields do @@ -343,26 +351,45 @@ defmodule MvWeb.MemberExportController do defp maybe_sort_export(query, field, order) when is_binary(field) do cond do field == "groups" -> - # Groups sort → in-memory nach dem Read (wie Tabelle) {query, true} + field == "membership_fee_type" -> + apply_membership_fee_type_sort_export(query, order) + custom_field_sort?(field) -> - # Custom field sort → in-memory nach dem Read (wie Tabelle) {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 + apply_member_field_sort_export(query, field, order) end rescue ArgumentError -> {query, false} end + defp apply_membership_fee_type_sort_export(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_member_field_sort_export(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 custom_field_sort?(field), do: String.starts_with?(field, @custom_field_prefix) # ------------------------------------------------------------------ @@ -488,6 +515,19 @@ defmodule MvWeb.MemberExportController do } end) + membership_fee_type_col = + if "membership_fee_type" in parsed.member_fields do + [ + %{ + header: membership_fee_type_field_header(conn), + kind: :membership_fee_type, + key: :membership_fee_type + } + ] + else + [] + end + groups_col = if "groups" in parsed.member_fields do [ @@ -519,7 +559,8 @@ defmodule MvWeb.MemberExportController 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 # --- headers: use MemberFields.label for translations --- @@ -559,6 +600,10 @@ defmodule MvWeb.MemberExportController do cf.name end + defp membership_fee_type_field_header(_conn) do + MemberFields.label(:membership_fee_type) + end + defp groups_field_header(_conn) do MemberFields.label(:groups) end