This commit is contained in:
parent
d34ff57531
commit
c82f4b7fd7
7 changed files with 487 additions and 1 deletions
|
|
@ -158,8 +158,9 @@ defmodule Mv.Authorization.PermissionSets do
|
|||
"/users/:id",
|
||||
"/users/:id/edit",
|
||||
"/users/:id/show/edit",
|
||||
# Member list
|
||||
# Member list and CSV export
|
||||
"/members",
|
||||
"/members/export.csv",
|
||||
# Member detail
|
||||
"/members/:id",
|
||||
# Custom field values overview
|
||||
|
|
@ -208,6 +209,7 @@ defmodule Mv.Authorization.PermissionSets do
|
|||
"/users/:id/edit",
|
||||
"/users/:id/show/edit",
|
||||
"/members",
|
||||
"/members/export.csv",
|
||||
# Create member
|
||||
"/members/new",
|
||||
"/members/:id",
|
||||
|
|
|
|||
55
lib/mv/membership/custom_field_value_formatter.ex
Normal file
55
lib/mv/membership/custom_field_value_formatter.ex
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
defmodule Mv.Membership.CustomFieldValueFormatter do
|
||||
@moduledoc """
|
||||
Neutral formatter for custom field values (e.g. CSV export).
|
||||
|
||||
Same logic as the member overview Formatter but without Gettext or web helpers,
|
||||
so it can be used from the Membership context. For boolean: "Yes"/"No";
|
||||
for date: European format (dd.mm.yyyy).
|
||||
"""
|
||||
@doc """
|
||||
Formats a custom field value for plain text (e.g. CSV).
|
||||
|
||||
Handles nil, Ash.Union, JSONB map, and direct values. Uses custom_field.value_type
|
||||
for typing. Boolean -> "Yes"/"No", Date -> dd.mm.yyyy.
|
||||
"""
|
||||
def format_custom_field_value(nil, _custom_field), do: ""
|
||||
|
||||
def format_custom_field_value(%Ash.Union{value: value, type: type}, custom_field) do
|
||||
format_value_by_type(value, type, custom_field)
|
||||
end
|
||||
|
||||
def format_custom_field_value(value, custom_field) when is_map(value) do
|
||||
type = Map.get(value, "type") || Map.get(value, "_union_type")
|
||||
val = Map.get(value, "value") || Map.get(value, "_union_value")
|
||||
format_value_by_type(val, type, custom_field)
|
||||
end
|
||||
|
||||
def format_custom_field_value(value, custom_field) do
|
||||
format_value_by_type(value, custom_field.value_type, custom_field)
|
||||
end
|
||||
|
||||
defp format_value_by_type(value, :string, _), do: to_string(value)
|
||||
defp format_value_by_type(value, :integer, _), do: to_string(value)
|
||||
|
||||
defp format_value_by_type(value, type, _) when type in [:string, :email] and is_binary(value) do
|
||||
if String.trim(value) == "", do: "", else: value
|
||||
end
|
||||
|
||||
defp format_value_by_type(value, :email, _), do: to_string(value)
|
||||
defp format_value_by_type(value, :boolean, _) when value == true, do: "Yes"
|
||||
defp format_value_by_type(value, :boolean, _) when value == false, do: "No"
|
||||
defp format_value_by_type(value, :boolean, _), do: to_string(value)
|
||||
|
||||
defp format_value_by_type(%Date{} = date, :date, _) do
|
||||
Calendar.strftime(date, "%d.%m.%Y")
|
||||
end
|
||||
|
||||
defp format_value_by_type(value, :date, _) when is_binary(value) do
|
||||
case Date.from_iso8601(value) do
|
||||
{:ok, date} -> Calendar.strftime(date, "%d.%m.%Y")
|
||||
_ -> value
|
||||
end
|
||||
end
|
||||
|
||||
defp format_value_by_type(value, _type, _), do: to_string(value)
|
||||
end
|
||||
91
lib/mv/membership/members_csv.ex
Normal file
91
lib/mv/membership/members_csv.ex
Normal file
|
|
@ -0,0 +1,91 @@
|
|||
defmodule Mv.Membership.MembersCSV do
|
||||
@moduledoc """
|
||||
Exports members to CSV (RFC 4180) as iodata.
|
||||
|
||||
Uses NimbleCSV.RFC4180 for encoding. Member fields are formatted as strings;
|
||||
custom field values use the same formatting logic as the member overview (neutral formatter).
|
||||
Column order for custom fields follows the key order of the `custom_fields_by_id` map.
|
||||
"""
|
||||
alias Mv.Membership.CustomFieldValueFormatter
|
||||
alias NimbleCSV.RFC4180
|
||||
|
||||
@doc """
|
||||
Exports a list of members to CSV iodata.
|
||||
|
||||
- `members` - List of member structs (with optional `custom_field_values` loaded)
|
||||
- `member_fields` - List of member field names (strings, e.g. `["first_name", "email"]`)
|
||||
- `custom_fields_by_id` - Map of custom_field_id => %CustomField{}. Key order defines column order.
|
||||
|
||||
Returns iodata suitable for `IO.iodata_to_binary/1` or sending as response body.
|
||||
"""
|
||||
@spec export(
|
||||
[struct()],
|
||||
[String.t()],
|
||||
%{optional(String.t() | Ecto.UUID.t()) => struct()}
|
||||
) :: iodata()
|
||||
def export(members, member_fields, custom_fields_by_id) when is_list(members) do
|
||||
custom_entries = custom_field_entries(custom_fields_by_id)
|
||||
header = build_header(member_fields, custom_entries)
|
||||
rows = Enum.map(members, &build_row(&1, member_fields, custom_entries))
|
||||
RFC4180.dump_to_iodata([header | rows])
|
||||
end
|
||||
|
||||
defp custom_field_entries(by_id) when is_map(by_id) do
|
||||
Enum.map(by_id, fn {id, cf} -> {to_string(id), cf} end)
|
||||
end
|
||||
|
||||
defp build_header(member_fields, custom_entries) do
|
||||
member_headers = member_fields
|
||||
custom_headers = Enum.map(custom_entries, fn {_id, cf} -> cf.name end)
|
||||
member_headers ++ custom_headers
|
||||
end
|
||||
|
||||
defp build_row(member, member_fields, custom_entries) do
|
||||
member_cells = Enum.map(member_fields, &format_member_field(member, &1))
|
||||
|
||||
custom_cells =
|
||||
Enum.map(custom_entries, fn {id, cf} -> format_custom_field(member, id, cf) end)
|
||||
|
||||
member_cells ++ custom_cells
|
||||
end
|
||||
|
||||
defp format_member_field(member, field_name) do
|
||||
key = member_field_key(field_name)
|
||||
value = Map.get(member, key)
|
||||
format_member_value(value)
|
||||
end
|
||||
|
||||
defp member_field_key(field_name) when is_binary(field_name) do
|
||||
try do
|
||||
String.to_existing_atom(field_name)
|
||||
rescue
|
||||
ArgumentError -> field_name
|
||||
end
|
||||
end
|
||||
|
||||
defp format_member_value(nil), do: ""
|
||||
defp format_member_value(true), do: "true"
|
||||
defp format_member_value(false), do: "false"
|
||||
defp format_member_value(%Date{} = d), do: Date.to_iso8601(d)
|
||||
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(value), do: to_string(value)
|
||||
|
||||
defp format_custom_field(member, custom_field_id, custom_field) do
|
||||
cfv = find_custom_field_value(member, custom_field_id)
|
||||
|
||||
if cfv,
|
||||
do: CustomFieldValueFormatter.format_custom_field_value(cfv.value, custom_field),
|
||||
else: ""
|
||||
end
|
||||
|
||||
defp find_custom_field_value(member, custom_field_id) do
|
||||
values = Map.get(member, :custom_field_values) || []
|
||||
id_str = to_string(custom_field_id)
|
||||
|
||||
Enum.find(values, fn cfv ->
|
||||
to_string(cfv.custom_field_id) == id_str or
|
||||
(Map.get(cfv, :custom_field) && to_string(cfv.custom_field.id) == id_str)
|
||||
end)
|
||||
end
|
||||
end
|
||||
Loading…
Add table
Add a link
Reference in a new issue