diff --git a/lib/mv_web/live/member_live/index/export_payload.ex b/lib/mv_web/live/member_live/index/export_payload.ex index 3c15c611..1ebd1dd2 100644 --- a/lib/mv_web/live/member_live/index/export_payload.ex +++ b/lib/mv_web/live/member_live/index/export_payload.ex @@ -29,10 +29,17 @@ defmodule MvWeb.MemberLive.Index.ExportPayload do member_fields_computed = assigns[:member_fields_visible_computed] || [] member_fields_visible = assigns[:member_fields_visible] || [] + # The composite Name/Address cells have no DB column of their own, so they are + # not in `member_fields_visible_db` — and their constituents are hidden from + # it precisely because the composite replaces them in the table. When a + # composite is the visible variant, export its constituent DB columns instead + # so the CSV/PDF still carries name and address, split into components. + composite_constituents = composite_export_constituents(member_fields_visible, assigns) + # Order DB member fields exactly like the table/constants. ordered_member_fields_db = Mv.Constants.member_fields() - |> Enum.filter(&(&1 in member_fields_db)) + |> Enum.filter(&(&1 in member_fields_db or &1 in composite_constituents)) # Order computed fields in canonical order. ordered_computed_fields = @@ -92,6 +99,26 @@ defmodule MvWeb.MemberLive.Index.ExportPayload do defp cycle_status_filter(:unpaid), do: "unpaid" defp cycle_status_filter(_), do: nil + # DB constituent columns to add for whichever composite cells are currently + # visible. Email is a name constituent only while folded into the compact + # Member cell (`member_include_email`); otherwise it is exported as its own + # visible column and needs no expansion here. + defp composite_export_constituents(member_fields_visible, assigns) do + name = + if :name in member_fields_visible do + FieldVisibility.name_constituents(assigns[:member_include_email] || false) + else + [] + end + + address = + if :address in member_fields_visible, + do: FieldVisibility.address_constituents(), + else: [] + + name ++ address + end + defp build_member_fields_list(ordered_db, member_fields_visible) do member_fields_visible = member_fields_visible || [] diff --git a/lib/mv_web/live/member_live/index/field_visibility.ex b/lib/mv_web/live/member_live/index/field_visibility.ex index e7baba78..68a4cefa 100644 --- a/lib/mv_web/live/member_live/index/field_visibility.ex +++ b/lib/mv_web/live/member_live/index/field_visibility.ex @@ -135,6 +135,20 @@ defmodule MvWeb.MemberLive.Index.FieldVisibility do leading ++ rest end + @doc """ + The DB constituent columns the composite `:name` cell stands in for, in + canonical order. `:email` is a constituent only while it is folded into the + compact Member cell (`include_email?` true); otherwise it is its own column. + """ + @spec name_constituents(boolean()) :: [atom()] + def name_constituents(include_email?) do + if include_email?, do: @name_constituents, else: @name_constituents -- [:email] + end + + @doc "The DB constituent columns the composite `:address` cell stands in for, in canonical order." + @spec address_constituents() :: [:street | :house_number | :postal_code | :city, ...] + def address_constituents, do: @address_constituents + # The email is a separate offered column except when it is folded into the # compact Member cell (compact + include_email). defp email_offered?(compact_member, include_email), do: not (compact_member and include_email) diff --git a/test/mv_web/live/member_live/index/export_payload_test.exs b/test/mv_web/live/member_live/index/export_payload_test.exs index 932816e6..2077d3d2 100644 --- a/test/mv_web/live/member_live/index/export_payload_test.exs +++ b/test/mv_web/live/member_live/index/export_payload_test.exs @@ -83,5 +83,59 @@ defmodule MvWeb.MemberLive.Index.ExportPayloadTest do assert payload.cycle_status_filter == nil assert payload.boolean_filters == %{} end + + test "expands visible composite Name/Address cells into their constituent db columns" do + # Compact mode: the composite :name / :address cells are visible and their + # constituent columns are hidden from member_fields_visible_db. The export + # must still carry name and address, split into their db components. + assigns = %{ + selected_members: MapSet.new(), + member_fields_visible_db: [:join_date], + member_fields_visible_computed: [], + member_fields_visible: [:name, :address, :join_date], + member_include_email: true, + visible_custom_field_ids: [], + all_custom_fields: [] + } + + payload = ExportPayload.build(assigns) + + # Constituents are emitted in Mv.Constants.member_fields/0 order, interleaved + # with the ordinary visible db field (join_date sits between the name and + # address groups per constants order). + assert payload.column_order == [ + "first_name", + "last_name", + "email", + "join_date", + "city", + "street", + "house_number", + "postal_code" + ] + + # The composite pseudo-columns themselves are never exported. + refute "name" in payload.member_fields + refute "address" in payload.member_fields + end + + test "omits email from the Name composite expansion when it is not folded into the cell" do + # include_email off: email is its own column (exported only if separately + # visible), so the Name composite expands to first/last name only. + assigns = %{ + selected_members: MapSet.new(), + member_fields_visible_db: [:join_date], + member_fields_visible_computed: [], + member_fields_visible: [:name, :join_date], + member_include_email: false, + visible_custom_field_ids: [], + all_custom_fields: [] + } + + payload = ExportPayload.build(assigns) + + assert payload.column_order == ["first_name", "last_name", "join_date"] + refute "email" in payload.column_order + end end end