Implements exporting groups closes #428 #435
6 changed files with 98 additions and 6 deletions
|
|
@ -16,7 +16,7 @@ defmodule Mv.Membership.MemberExport do
|
||||||
alias MvWeb.MemberLive.Index.MembershipFeeStatus
|
alias MvWeb.MemberLive.Index.MembershipFeeStatus
|
||||||
|
|
||||||
@member_fields_allowlist (Mv.Constants.member_fields() |> Enum.map(&Atom.to_string/1)) ++
|
@member_fields_allowlist (Mv.Constants.member_fields() |> Enum.map(&Atom.to_string/1)) ++
|
||||||
["membership_fee_status"]
|
["membership_fee_status", "groups"]
|
||||||
@computed_export_fields ["membership_fee_status"]
|
@computed_export_fields ["membership_fee_status"]
|
||||||
@computed_insert_after "membership_fee_start_date"
|
@computed_insert_after "membership_fee_start_date"
|
||||||
@custom_field_prefix Mv.Constants.custom_field_prefix()
|
@custom_field_prefix Mv.Constants.custom_field_prefix()
|
||||||
|
|
@ -323,10 +323,14 @@ defmodule Mv.Membership.MemberExport do
|
||||||
|> Enum.filter(&(&1 in @domain_member_field_strings))
|
|> Enum.filter(&(&1 in @domain_member_field_strings))
|
||||||
|> order_member_fields_like_table()
|
|> order_member_fields_like_table()
|
||||||
|
|
||||||
# final member_fields list (used for column specs order): table order + computed inserted
|
# 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
|
||||||
ordered_member_fields =
|
ordered_member_fields =
|
||||||
selectable_member_fields
|
selectable_member_fields
|
||||||
|> insert_computed_fields_like_table(computed_fields)
|
|> insert_computed_fields_like_table(computed_fields)
|
||||||
|
|> then(fn fields -> fields ++ groups_field end)
|
||||||
|
|
||||||
%{
|
%{
|
||||||
selected_ids: filter_valid_uuids(extract_list(params, "selected_ids")),
|
selected_ids: filter_valid_uuids(extract_list(params, "selected_ids")),
|
||||||
|
|
|
||||||
|
|
@ -132,12 +132,15 @@ defmodule Mv.Membership.MemberExport.Build do
|
||||||
parsed.computed_fields != [] or
|
parsed.computed_fields != [] or
|
||||||
"membership_fee_status" in parsed.member_fields
|
"membership_fee_status" in parsed.member_fields
|
||||||
|
|
||||||
|
need_groups = "groups" in parsed.member_fields
|
||||||
|
|
||||||
query =
|
query =
|
||||||
Member
|
Member
|
||||||
|> Ash.Query.new()
|
|> Ash.Query.new()
|
||||||
|> Ash.Query.select(select_fields)
|
|> Ash.Query.select(select_fields)
|
||||||
|> load_custom_field_values_query(custom_field_ids_union)
|
|> load_custom_field_values_query(custom_field_ids_union)
|
||||||
|> maybe_load_cycles(need_cycles, parsed.show_current_cycle)
|
|> maybe_load_cycles(need_cycles, parsed.show_current_cycle)
|
||||||
|
|> maybe_load_groups(need_groups)
|
||||||
|
|
||||||
query =
|
query =
|
||||||
if parsed.selected_ids != [] do
|
if parsed.selected_ids != [] do
|
||||||
|
|
@ -294,6 +297,13 @@ defmodule Mv.Membership.MemberExport.Build do
|
||||||
MembershipFeeStatus.load_cycles_for_members(query, show_current)
|
MembershipFeeStatus.load_cycles_for_members(query, show_current)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
defp maybe_load_groups(query, false), do: query
|
||||||
|
|
||||||
|
defp maybe_load_groups(query, true) do
|
||||||
|
# Load groups with id and name only (for export formatting)
|
||||||
|
Ash.Query.load(query, groups: [:id, :name])
|
||||||
|
end
|
||||||
|
|
||||||
defp apply_cycle_status_filter(members, nil, _show_current), do: members
|
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
|
defp apply_cycle_status_filter(members, status, show_current) when status in [:paid, :unpaid] do
|
||||||
|
|
@ -343,6 +353,19 @@ defmodule Mv.Membership.MemberExport.Build do
|
||||||
}
|
}
|
||||||
end)
|
end)
|
||||||
|
|
||||||
|
groups_col =
|
||||||
|
if "groups" in parsed.member_fields do
|
||||||
|
[
|
||||||
|
%{
|
||||||
|
key: :groups,
|
||||||
|
kind: :groups,
|
||||||
|
label: label_fn.(:groups)
|
||||||
|
}
|
||||||
|
]
|
||||||
|
else
|
||||||
|
[]
|
||||||
|
end
|
||||||
|
|
||||||
custom_cols =
|
custom_cols =
|
||||||
parsed.custom_field_ids
|
parsed.custom_field_ids
|
||||||
|> Enum.map(fn id ->
|
|> Enum.map(fn id ->
|
||||||
|
|
@ -361,7 +384,7 @@ defmodule Mv.Membership.MemberExport.Build do
|
||||||
end)
|
end)
|
||||||
|> Enum.reject(&is_nil/1)
|
|> Enum.reject(&is_nil/1)
|
||||||
|
|
||||||
member_cols ++ computed_cols ++ custom_cols
|
member_cols ++ computed_cols ++ groups_col ++ custom_cols
|
||||||
end
|
end
|
||||||
|
|
||||||
defp build_rows(members, columns, custom_fields_by_id) do
|
defp build_rows(members, columns, custom_fields_by_id) do
|
||||||
|
|
@ -391,6 +414,11 @@ defmodule Mv.Membership.MemberExport.Build do
|
||||||
if is_binary(value), do: value, else: ""
|
if is_binary(value), do: value, else: ""
|
||||||
end
|
end
|
||||||
|
|
||||||
|
defp cell_value(member, %{kind: :groups, key: :groups}, _custom_fields_by_id) do
|
||||||
|
groups = Map.get(member, :groups) || []
|
||||||
|
format_groups(groups)
|
||||||
|
end
|
||||||
|
|
||||||
defp key_to_atom(k) when is_atom(k), do: k
|
defp key_to_atom(k) when is_atom(k), do: k
|
||||||
|
|
||||||
defp key_to_atom(k) when is_binary(k) do
|
defp key_to_atom(k) when is_binary(k) do
|
||||||
|
|
@ -424,6 +452,15 @@ defmodule Mv.Membership.MemberExport.Build do
|
||||||
defp format_member_value(%NaiveDateTime{} = dt), do: NaiveDateTime.to_iso8601(dt)
|
defp format_member_value(%NaiveDateTime{} = dt), do: NaiveDateTime.to_iso8601(dt)
|
||||||
defp format_member_value(value), do: to_string(value)
|
defp format_member_value(value), do: to_string(value)
|
||||||
|
|
||||||
|
defp format_groups([]), do: ""
|
||||||
|
|
||||||
|
defp format_groups(groups) when is_list(groups) do
|
||||||
|
groups
|
||||||
|
|> Enum.map(fn group -> Map.get(group, :name) || "" end)
|
||||||
|
|> Enum.reject(&(&1 == ""))
|
||||||
|
|> Enum.join(", ")
|
||||||
|
end
|
||||||
|
|
||||||
defp build_meta(members) do
|
defp build_meta(members) do
|
||||||
%{
|
%{
|
||||||
generated_at: DateTime.utc_now() |> DateTime.to_iso8601(),
|
generated_at: DateTime.utc_now() |> DateTime.to_iso8601(),
|
||||||
|
|
|
||||||
|
|
@ -59,6 +59,11 @@ defmodule Mv.Membership.MembersCSV do
|
||||||
if is_binary(value), do: value, else: ""
|
if is_binary(value), do: value, else: ""
|
||||||
end
|
end
|
||||||
|
|
||||||
|
defp cell_value(member, %{kind: :groups, key: :groups}) do
|
||||||
|
groups = Map.get(member, :groups) || []
|
||||||
|
format_groups(groups)
|
||||||
|
end
|
||||||
|
|
||||||
defp key_to_atom(k) when is_atom(k), do: k
|
defp key_to_atom(k) when is_atom(k), do: k
|
||||||
|
|
||||||
defp key_to_atom(k) when is_binary(k) do
|
defp key_to_atom(k) when is_binary(k) do
|
||||||
|
|
@ -97,4 +102,13 @@ defmodule Mv.Membership.MembersCSV do
|
||||||
defp format_member_value(%DateTime{} = dt), do: DateTime.to_iso8601(dt)
|
defp format_member_value(%DateTime{} = dt), do: DateTime.to_iso8601(dt)
|
||||||
defp format_member_value(%NaiveDateTime{} = dt), do: NaiveDateTime.to_iso8601(dt)
|
defp format_member_value(%NaiveDateTime{} = dt), do: NaiveDateTime.to_iso8601(dt)
|
||||||
defp format_member_value(value), do: to_string(value)
|
defp format_member_value(value), do: to_string(value)
|
||||||
|
|
||||||
|
defp format_groups([]), do: ""
|
||||||
|
|
||||||
|
defp format_groups(groups) when is_list(groups) do
|
||||||
|
groups
|
||||||
|
|> Enum.map(fn group -> Map.get(group, :name) || "" end)
|
||||||
|
|> Enum.reject(&(&1 == ""))
|
||||||
|
|> Enum.join(", ")
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,8 @@ defmodule MvWeb.MemberExportController do
|
||||||
alias MvWeb.MemberLive.Index.MembershipFeeStatus
|
alias MvWeb.MemberLive.Index.MembershipFeeStatus
|
||||||
use Gettext, backend: MvWeb.Gettext
|
use Gettext, backend: MvWeb.Gettext
|
||||||
|
|
||||||
@member_fields_allowlist Mv.Constants.member_fields() |> Enum.map(&Atom.to_string/1)
|
@member_fields_allowlist (Mv.Constants.member_fields() |> Enum.map(&Atom.to_string/1)) ++
|
||||||
|
["groups"]
|
||||||
@computed_export_fields ["membership_fee_status"]
|
@computed_export_fields ["membership_fee_status"]
|
||||||
@custom_field_prefix Mv.Constants.custom_field_prefix()
|
@custom_field_prefix Mv.Constants.custom_field_prefix()
|
||||||
|
|
||||||
|
|
@ -83,6 +84,7 @@ defmodule MvWeb.MemberExportController do
|
||||||
domain_fields = Mv.Constants.member_fields() |> Enum.map(&Atom.to_string/1)
|
domain_fields = Mv.Constants.member_fields() |> Enum.map(&Atom.to_string/1)
|
||||||
selectable = Enum.filter(member_fields, fn f -> f in domain_fields end)
|
selectable = Enum.filter(member_fields, fn f -> f in domain_fields end)
|
||||||
computed = Enum.filter(member_fields, fn f -> f in @computed_export_fields end)
|
computed = Enum.filter(member_fields, fn f -> f in @computed_export_fields end)
|
||||||
|
# "groups" is neither a domain field nor a computed field, it's handled separately
|
||||||
{selectable, computed}
|
{selectable, computed}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -235,12 +237,15 @@ defmodule MvWeb.MemberExportController do
|
||||||
need_cycles =
|
need_cycles =
|
||||||
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
|
||||||
|
|
||||||
query =
|
query =
|
||||||
Member
|
Member
|
||||||
|> Ash.Query.new()
|
|> Ash.Query.new()
|
||||||
|> Ash.Query.select(select_fields)
|
|> Ash.Query.select(select_fields)
|
||||||
|> load_custom_field_values_query(parsed.custom_field_ids)
|
|> load_custom_field_values_query(parsed.custom_field_ids)
|
||||||
|> maybe_load_cycles(need_cycles, parsed.show_current_cycle)
|
|> maybe_load_cycles(need_cycles, parsed.show_current_cycle)
|
||||||
|
|> maybe_load_groups(need_groups)
|
||||||
|
|
||||||
query =
|
query =
|
||||||
if parsed.selected_ids != [] do
|
if parsed.selected_ids != [] do
|
||||||
|
|
@ -284,6 +289,13 @@ defmodule MvWeb.MemberExportController do
|
||||||
MembershipFeeStatus.load_cycles_for_members(query, show_current)
|
MembershipFeeStatus.load_cycles_for_members(query, show_current)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
defp maybe_load_groups(query, false), do: query
|
||||||
|
|
||||||
|
defp maybe_load_groups(query, true) do
|
||||||
|
# Load groups with id and name only (for export formatting)
|
||||||
|
Ash.Query.load(query, groups: [:id, :name])
|
||||||
|
end
|
||||||
|
|
||||||
# Adds computed field values to members (e.g. membership_fee_status)
|
# Adds computed field values to members (e.g. membership_fee_status)
|
||||||
defp add_computed_fields(members, computed_fields, show_current_cycle) do
|
defp add_computed_fields(members, computed_fields, show_current_cycle) do
|
||||||
if "membership_fee_status" in computed_fields do
|
if "membership_fee_status" in computed_fields do
|
||||||
|
|
@ -441,6 +453,19 @@ defmodule MvWeb.MemberExportController do
|
||||||
}
|
}
|
||||||
end)
|
end)
|
||||||
|
|
||||||
|
groups_col =
|
||||||
|
if "groups" in parsed.member_fields do
|
||||||
|
[
|
||||||
|
%{
|
||||||
|
header: groups_field_header(conn),
|
||||||
|
kind: :groups,
|
||||||
|
key: :groups
|
||||||
|
}
|
||||||
|
]
|
||||||
|
else
|
||||||
|
[]
|
||||||
|
end
|
||||||
|
|
||||||
custom_cols =
|
custom_cols =
|
||||||
parsed.custom_field_ids
|
parsed.custom_field_ids
|
||||||
|> Enum.map(fn id ->
|
|> Enum.map(fn id ->
|
||||||
|
|
@ -459,7 +484,7 @@ defmodule MvWeb.MemberExportController do
|
||||||
end)
|
end)
|
||||||
|> Enum.reject(&is_nil/1)
|
|> Enum.reject(&is_nil/1)
|
||||||
|
|
||||||
member_cols ++ computed_cols ++ custom_cols
|
member_cols ++ computed_cols ++ groups_col ++ custom_cols
|
||||||
end
|
end
|
||||||
|
|
||||||
# --- headers: use MemberFields.label for translations ---
|
# --- headers: use MemberFields.label for translations ---
|
||||||
|
|
@ -499,6 +524,10 @@ defmodule MvWeb.MemberExportController do
|
||||||
cf.name
|
cf.name
|
||||||
end
|
end
|
||||||
|
|
||||||
|
defp groups_field_header(_conn) do
|
||||||
|
MemberFields.label(:groups)
|
||||||
|
end
|
||||||
|
|
||||||
defp humanize_field(str) do
|
defp humanize_field(str) do
|
||||||
str
|
str
|
||||||
|> String.replace("_", " ")
|
|> String.replace("_", " ")
|
||||||
|
|
|
||||||
|
|
@ -1620,6 +1620,10 @@ defmodule MvWeb.MemberLive.Index do
|
||||||
FieldVisibility.computed_member_fields()
|
FieldVisibility.computed_member_fields()
|
||||||
|> Enum.filter(&(&1 in member_fields_computed))
|
|> Enum.filter(&(&1 in member_fields_computed))
|
||||||
|
|
||||||
|
# Groups is always included in export if it's visible in the table
|
||||||
|
# (groups column is always shown in the table, so we always include it in export)
|
||||||
|
member_fields_with_groups = ordered_member_fields_db ++ ["groups"]
|
||||||
|
|
||||||
# Order custom fields like the table (same as dynamic_cols / all_custom_fields order)
|
# Order custom fields like the table (same as dynamic_cols / all_custom_fields order)
|
||||||
ordered_custom_field_ids =
|
ordered_custom_field_ids =
|
||||||
socket.assigns.all_custom_fields
|
socket.assigns.all_custom_fields
|
||||||
|
|
@ -1628,7 +1632,10 @@ defmodule MvWeb.MemberLive.Index do
|
||||||
|
|
||||||
%{
|
%{
|
||||||
selected_ids: socket.assigns.selected_members |> MapSet.to_list(),
|
selected_ids: socket.assigns.selected_members |> MapSet.to_list(),
|
||||||
member_fields: Enum.map(ordered_member_fields_db, &Atom.to_string/1),
|
member_fields: Enum.map(member_fields_with_groups, fn
|
||||||
|
f when is_atom(f) -> Atom.to_string(f)
|
||||||
|
f when is_binary(f) -> f
|
||||||
|
end),
|
||||||
computed_fields: Enum.map(ordered_computed_fields, &Atom.to_string/1),
|
computed_fields: Enum.map(ordered_computed_fields, &Atom.to_string/1),
|
||||||
custom_field_ids: ordered_custom_field_ids,
|
custom_field_ids: ordered_custom_field_ids,
|
||||||
column_order:
|
column_order:
|
||||||
|
|
|
||||||
|
|
@ -29,6 +29,7 @@ defmodule MvWeb.Translations.MemberFields do
|
||||||
def label(:postal_code), do: gettext("Postal Code")
|
def label(:postal_code), do: gettext("Postal Code")
|
||||||
def label(:membership_fee_start_date), do: gettext("Membership Fee Start Date")
|
def label(:membership_fee_start_date), do: gettext("Membership Fee Start Date")
|
||||||
def label(:membership_fee_status), do: gettext("Membership Fee Status")
|
def label(:membership_fee_status), do: gettext("Membership Fee Status")
|
||||||
|
def label(:groups), do: gettext("Groups")
|
||||||
|
|
||||||
# Fallback for unknown fields
|
# Fallback for unknown fields
|
||||||
def label(field) do
|
def label(field) do
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue