654 lines
23 KiB
Elixir
654 lines
23 KiB
Elixir
defmodule MvWeb.MemberLive.Form do
|
|
@moduledoc """
|
|
LiveView form for creating and editing members.
|
|
|
|
## Features
|
|
- Create new members with personal information
|
|
- Edit existing member details
|
|
- Grouped sections for better organization
|
|
- Tab navigation (Payments tab disabled, coming soon)
|
|
- Manage custom properties (dynamic fields, displayed sorted by name)
|
|
- Real-time validation with visual feedback
|
|
|
|
## Form Sections
|
|
- Personal Data: Name, address, contact information, membership dates, notes
|
|
- Custom Fields: Dynamic fields in uniform grid layout (displayed sorted by name)
|
|
- Payment Data: Mockup section (not editable)
|
|
|
|
## Events
|
|
- `validate` - Real-time form validation
|
|
- `save` - Submit form (create or update member)
|
|
"""
|
|
use MvWeb, :live_view
|
|
|
|
on_mount {MvWeb.LiveHelpers, :ensure_user_role_loaded}
|
|
|
|
import MvWeb.LiveHelpers, only: [current_actor: 1, submit_form: 3]
|
|
|
|
alias Mv.MembershipFees
|
|
alias Mv.MembershipFees.MembershipFeeType
|
|
alias MvWeb.Helpers.MembershipFeeHelpers
|
|
|
|
@impl true
|
|
def render(assigns) do
|
|
# Sort custom fields by name for display only
|
|
sorted_custom_fields = Enum.sort_by(assigns.custom_fields, & &1.name)
|
|
assigns = assign(assigns, :sorted_custom_fields, sorted_custom_fields)
|
|
|
|
~H"""
|
|
<Layouts.app flash={@flash} current_user={@current_user}>
|
|
<.form for={@form} id="member-form" phx-change="validate" phx-submit="save">
|
|
<%!-- Header with Back button, Name display, and Save button --%>
|
|
<div class="flex items-center justify-between gap-4 pb-4">
|
|
<.button navigate={return_path(@return_to, @member)} type="button">
|
|
<.icon name="hero-arrow-left" class="size-4" />
|
|
{gettext("Back")}
|
|
</.button>
|
|
|
|
<h1 class="text-2xl font-bold text-center flex-1">
|
|
<%= if @member do %>
|
|
{MvWeb.Helpers.MemberHelpers.display_name(@member)}
|
|
<% else %>
|
|
{gettext("New Member")}
|
|
<% end %>
|
|
</h1>
|
|
|
|
<.button phx-disable-with={gettext("Saving...")} variant="primary" type="submit">
|
|
{gettext("Save")}
|
|
</.button>
|
|
</div>
|
|
|
|
<%!-- Tab Navigation --%>
|
|
<div role="tablist" class="tabs tabs-bordered mb-6">
|
|
<button type="button" role="tab" class="tab tab-active" aria-selected="true">
|
|
<.icon name="hero-identification" class="size-4 mr-2" />
|
|
{gettext("Contact Data")}
|
|
</button>
|
|
<button
|
|
type="button"
|
|
role="tab"
|
|
class="tab"
|
|
disabled
|
|
aria-disabled="true"
|
|
title={gettext("Coming soon")}
|
|
>
|
|
<.icon name="hero-credit-card" class="size-4 mr-2" />
|
|
{gettext("Payments")}
|
|
</button>
|
|
</div>
|
|
|
|
<%!-- Personal Data and Custom Fields Row --%>
|
|
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-6">
|
|
<%!-- Personal Data Section --%>
|
|
<div>
|
|
<.form_section title={gettext("Personal Data")}>
|
|
<div class="space-y-4">
|
|
<%!-- Name Row --%>
|
|
<div class="flex gap-4">
|
|
<div class="w-48">
|
|
<.input field={@form[:first_name]} label={gettext("First Name")} />
|
|
</div>
|
|
<div class="w-48">
|
|
<.input field={@form[:last_name]} label={gettext("Last Name")} />
|
|
</div>
|
|
</div>
|
|
|
|
<%!-- Address Row --%>
|
|
<div class="flex gap-4">
|
|
<div class="flex-1">
|
|
<.input field={@form[:street]} label={gettext("Street")} />
|
|
</div>
|
|
<div class="w-16">
|
|
<.input field={@form[:house_number]} label={gettext("Nr.")} />
|
|
</div>
|
|
<div class="w-24">
|
|
<.input field={@form[:postal_code]} label={gettext("Postal Code")} />
|
|
</div>
|
|
<div class="w-32">
|
|
<.input field={@form[:city]} label={gettext("City")} />
|
|
</div>
|
|
</div>
|
|
|
|
<%!-- Email --%>
|
|
<div>
|
|
<.input field={@form[:email]} label={gettext("Email")} required type="email" />
|
|
</div>
|
|
|
|
<%!-- Membership Dates Row --%>
|
|
<div class="flex gap-4">
|
|
<div class="w-36">
|
|
<.input field={@form[:join_date]} label={gettext("Join Date")} type="date" />
|
|
</div>
|
|
<div class="w-36">
|
|
<.input field={@form[:exit_date]} label={gettext("Exit Date")} type="date" />
|
|
</div>
|
|
</div>
|
|
|
|
<%!-- Notes --%>
|
|
<div>
|
|
<.input field={@form[:notes]} label={gettext("Notes")} type="textarea" />
|
|
</div>
|
|
</div>
|
|
</.form_section>
|
|
</div>
|
|
|
|
<%!-- Custom Fields Section --%>
|
|
<%= if Enum.any?(@custom_fields) do %>
|
|
<div>
|
|
<.form_section title={gettext("Custom Fields")}>
|
|
<div class="grid grid-cols-2 gap-4">
|
|
<%!-- Render in sorted order by finding the form for each sorted custom field --%>
|
|
<%= for cf <- @sorted_custom_fields do %>
|
|
<.inputs_for :let={f_cfv} field={@form[:custom_field_values]}>
|
|
<%= if f_cfv[:custom_field_id].value == cf.id do %>
|
|
<div class={if cf.value_type == :boolean, do: "flex items-end", else: ""}>
|
|
<.inputs_for :let={value_form} field={f_cfv[:value]}>
|
|
<.input
|
|
field={value_form[:value]}
|
|
label={cf.name}
|
|
type={custom_field_input_type(cf.value_type)}
|
|
required={cf.required}
|
|
/>
|
|
</.inputs_for>
|
|
<input
|
|
type="hidden"
|
|
name={f_cfv[:custom_field_id].name}
|
|
value={f_cfv[:custom_field_id].value}
|
|
/>
|
|
</div>
|
|
<% end %>
|
|
</.inputs_for>
|
|
<% end %>
|
|
</div>
|
|
</.form_section>
|
|
</div>
|
|
<% end %>
|
|
</div>
|
|
|
|
<%!-- Membership Fee Section --%>
|
|
<div class="max-w-xl">
|
|
<.form_section title={gettext("Membership Fee")}>
|
|
<div class="space-y-4">
|
|
<div>
|
|
<label class="label">
|
|
<span class="label-text font-semibold">{gettext("Membership Fee Type")}</span>
|
|
</label>
|
|
<select
|
|
class="select select-bordered w-full"
|
|
name={@form[:membership_fee_type_id].name}
|
|
phx-change="validate"
|
|
value={@form[:membership_fee_type_id].value || ""}
|
|
>
|
|
<option value="">{gettext("None")}</option>
|
|
<%= for fee_type <- @available_fee_types do %>
|
|
<option
|
|
value={fee_type.id}
|
|
selected={fee_type.id == @form[:membership_fee_type_id].value}
|
|
>
|
|
{fee_type.name} ({MembershipFeeHelpers.format_currency(fee_type.amount)}, {MembershipFeeHelpers.format_interval(
|
|
fee_type.interval
|
|
)})
|
|
</option>
|
|
<% end %>
|
|
</select>
|
|
<%= for {msg, _opts} <- @form.errors[:membership_fee_type_id] || [] do %>
|
|
<p class="text-error text-sm mt-1">{msg}</p>
|
|
<% end %>
|
|
<%= if @interval_warning do %>
|
|
<div class="alert alert-warning mt-2">
|
|
<.icon name="hero-exclamation-triangle" class="size-5" />
|
|
<span>{@interval_warning}</span>
|
|
</div>
|
|
<% end %>
|
|
<p class="text-sm text-base-content/60 mt-2">
|
|
{gettext(
|
|
"Select a membership fee type for this member. Members can only switch between types with the same interval."
|
|
)}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</.form_section>
|
|
</div>
|
|
|
|
<%!-- Bottom Action Buttons --%>
|
|
<div class="flex justify-end gap-4 mt-6">
|
|
<.button navigate={return_path(@return_to, @member)} type="button">
|
|
{gettext("Cancel")}
|
|
</.button>
|
|
<.button phx-disable-with={gettext("Saving...")} variant="primary" type="submit">
|
|
{gettext("Save Member")}
|
|
</.button>
|
|
</div>
|
|
</.form>
|
|
</Layouts.app>
|
|
"""
|
|
end
|
|
|
|
@impl true
|
|
def mount(params, _session, socket) do
|
|
# current_user should be set by on_mount hooks (LiveUserAuth + LiveHelpers)
|
|
actor = current_actor(socket)
|
|
{:ok, custom_fields} = Mv.Membership.list_custom_fields()
|
|
|
|
initial_custom_field_values =
|
|
Enum.map(custom_fields, fn cf ->
|
|
%{
|
|
"custom_field_id" => cf.id,
|
|
"value" => %{
|
|
"type" => cf.value_type,
|
|
"value" => nil,
|
|
"_union_type" => Atom.to_string(cf.value_type)
|
|
}
|
|
}
|
|
end)
|
|
|
|
member =
|
|
case params["id"] do
|
|
nil -> nil
|
|
id -> Ash.get!(Mv.Membership.Member, id, load: [:membership_fee_type], actor: actor)
|
|
end
|
|
|
|
page_title =
|
|
if is_nil(member), do: gettext("Create Member"), else: gettext("Edit Member")
|
|
|
|
# Load available membership fee types
|
|
available_fee_types = load_available_fee_types(member, actor)
|
|
|
|
{:ok,
|
|
socket
|
|
|> assign(:return_to, return_to(params["return_to"]))
|
|
|> assign(:custom_fields, custom_fields)
|
|
|> assign(:initial_custom_field_values, initial_custom_field_values)
|
|
|> assign(member: member)
|
|
|> assign(:page_title, page_title)
|
|
|> assign(:available_fee_types, available_fee_types)
|
|
|> assign(:interval_warning, nil)
|
|
|> assign_form()}
|
|
end
|
|
|
|
defp return_to("show"), do: "show"
|
|
defp return_to(_), do: "index"
|
|
|
|
@impl true
|
|
def handle_event("validate", %{"member" => member_params}, socket) do
|
|
# Merge with existing form values to preserve unchanged fields (especially custom_field_values)
|
|
# Extract values directly from form fields to get current state
|
|
existing_values = get_existing_form_values(socket.assigns.form)
|
|
|
|
# Merge existing values with new params (new params take precedence)
|
|
merged_params = Map.merge(existing_values, member_params)
|
|
|
|
validated_form = AshPhoenix.Form.validate(socket.assigns.form, merged_params)
|
|
|
|
# Check for interval mismatch if membership_fee_type_id changed
|
|
socket = check_interval_change(socket, merged_params)
|
|
|
|
{:noreply, assign(socket, form: validated_form)}
|
|
end
|
|
|
|
def handle_event("save", %{"member" => member_params}, socket) do
|
|
try do
|
|
actor = current_actor(socket)
|
|
|
|
case submit_form(socket.assigns.form, member_params, actor) do
|
|
{:ok, member} ->
|
|
handle_save_success(socket, member)
|
|
|
|
{:error, form} ->
|
|
{:noreply, assign(socket, form: form)}
|
|
end
|
|
rescue
|
|
_e in [Ash.Error.Forbidden, Ash.Error.Forbidden.Policy] ->
|
|
handle_save_forbidden(socket)
|
|
end
|
|
end
|
|
|
|
defp handle_save_success(socket, member) do
|
|
notify_parent({:saved, member})
|
|
|
|
flash_message =
|
|
case socket.assigns.form.source.type do
|
|
:create -> gettext("Member created successfully")
|
|
:update -> gettext("Member updated successfully")
|
|
other -> gettext("Member %{action} successfully", action: get_action_name(other))
|
|
end
|
|
|
|
socket =
|
|
socket
|
|
|> put_flash(:info, flash_message)
|
|
|> push_navigate(to: return_path(socket.assigns.return_to, member))
|
|
|
|
{:noreply, socket}
|
|
end
|
|
|
|
defp handle_save_forbidden(socket) do
|
|
# Handle policy violations that aren't properly displayed in forms
|
|
# AshPhoenix.Form doesn't implement FormData.Error protocol for Forbidden errors
|
|
action = get_action_name(socket.assigns.form.source.type)
|
|
|
|
error_message =
|
|
gettext("You do not have permission to %{action} members.", action: action)
|
|
|
|
{:noreply, put_flash(socket, :error, error_message)}
|
|
end
|
|
|
|
defp get_action_name(:create), do: gettext("create")
|
|
defp get_action_name(:update), do: gettext("update")
|
|
defp get_action_name(other), do: to_string(other)
|
|
|
|
defp notify_parent(msg), do: send(self(), {__MODULE__, msg})
|
|
|
|
defp assign_form(%{assigns: assigns} = socket) do
|
|
member = assigns.member
|
|
actor = assigns[:current_user] || assigns.current_user
|
|
|
|
form =
|
|
if member do
|
|
{:ok, member} = Ash.load(member, [custom_field_values: [:custom_field]], actor: actor)
|
|
|
|
existing_custom_field_values =
|
|
member.custom_field_values
|
|
|> Enum.map(& &1.custom_field_id)
|
|
|
|
is_missing_custom_field_value = fn i ->
|
|
not Enum.member?(existing_custom_field_values, Map.get(i, "custom_field_id"))
|
|
end
|
|
|
|
params = %{
|
|
"custom_field_values" =>
|
|
Enum.map(member.custom_field_values, fn cfv ->
|
|
%{
|
|
"custom_field_id" => cfv.custom_field_id,
|
|
"value" => %{
|
|
"_union_type" => Atom.to_string(cfv.value.type),
|
|
"type" => cfv.value.type,
|
|
"value" => cfv.value.value
|
|
}
|
|
}
|
|
end)
|
|
}
|
|
|
|
form =
|
|
AshPhoenix.Form.for_update(
|
|
member,
|
|
:update_member,
|
|
api: Mv.Membership,
|
|
as: "member",
|
|
params: params,
|
|
forms: [auto?: true],
|
|
actor: actor
|
|
)
|
|
|
|
missing_custom_field_values =
|
|
Enum.filter(socket.assigns[:initial_custom_field_values], is_missing_custom_field_value)
|
|
|
|
Enum.reduce(
|
|
missing_custom_field_values,
|
|
form,
|
|
&AshPhoenix.Form.add_form(&2, [:custom_field_values], params: &1)
|
|
)
|
|
else
|
|
AshPhoenix.Form.for_create(
|
|
Mv.Membership.Member,
|
|
:create_member,
|
|
api: Mv.Membership,
|
|
as: "member",
|
|
params: %{"custom_field_values" => socket.assigns[:initial_custom_field_values]},
|
|
forms: [auto?: true],
|
|
actor: actor
|
|
)
|
|
end
|
|
|
|
assign(socket, form: to_form(form))
|
|
end
|
|
|
|
defp return_path("index", _member), do: ~p"/members"
|
|
defp return_path("show", nil), do: ~p"/members"
|
|
defp return_path("show", member), do: ~p"/members/#{member.id}"
|
|
|
|
# -----------------------------------------------------------------
|
|
# Helper Functions
|
|
# -----------------------------------------------------------------
|
|
|
|
defp load_available_fee_types(member, actor) do
|
|
all_types =
|
|
MembershipFeeType
|
|
|> Ash.Query.sort(name: :asc)
|
|
|> Ash.read!(domain: MembershipFees, actor: actor)
|
|
|
|
# If member has a fee type, filter to same interval
|
|
if member && member.membership_fee_type do
|
|
Enum.filter(all_types, fn type ->
|
|
type.interval == member.membership_fee_type.interval
|
|
end)
|
|
else
|
|
all_types
|
|
end
|
|
end
|
|
|
|
# Checks if membership fee type interval changed and updates socket assigns
|
|
defp check_interval_change(socket, member_params) do
|
|
if Map.has_key?(member_params, "membership_fee_type_id") &&
|
|
socket.assigns.member &&
|
|
socket.assigns.member.membership_fee_type do
|
|
handle_interval_change(socket, member_params["membership_fee_type_id"])
|
|
else
|
|
socket
|
|
end
|
|
end
|
|
|
|
# Handles interval change validation
|
|
defp handle_interval_change(socket, new_fee_type_id) do
|
|
if new_fee_type_id != "" &&
|
|
new_fee_type_id != socket.assigns.member.membership_fee_type_id do
|
|
validate_interval_match(socket, new_fee_type_id)
|
|
else
|
|
assign(socket, :interval_warning, nil)
|
|
end
|
|
end
|
|
|
|
# Validates that new fee type has same interval as current
|
|
defp validate_interval_match(socket, new_fee_type_id) do
|
|
new_fee_type = find_fee_type(socket.assigns.available_fee_types, new_fee_type_id)
|
|
|
|
if new_fee_type &&
|
|
new_fee_type.interval != socket.assigns.member.membership_fee_type.interval do
|
|
show_interval_warning(socket, new_fee_type)
|
|
else
|
|
assign(socket, :interval_warning, nil)
|
|
end
|
|
end
|
|
|
|
# Shows interval mismatch warning
|
|
defp show_interval_warning(socket, new_fee_type) do
|
|
assign(
|
|
socket,
|
|
:interval_warning,
|
|
gettext(
|
|
"Warning: Changing from %{old_interval} to %{new_interval} is not allowed. Please select a membership fee type with the same interval.",
|
|
old_interval:
|
|
MembershipFeeHelpers.format_interval(socket.assigns.member.membership_fee_type.interval),
|
|
new_interval: MembershipFeeHelpers.format_interval(new_fee_type.interval)
|
|
)
|
|
)
|
|
end
|
|
|
|
defp find_fee_type(fee_types, fee_type_id) do
|
|
Enum.find(fee_types, &(&1.id == fee_type_id))
|
|
end
|
|
|
|
# -----------------------------------------------------------------
|
|
# Helper Functions for Custom Fields
|
|
# -----------------------------------------------------------------
|
|
|
|
# Returns input type for custom field based on value type
|
|
defp custom_field_input_type(:string), do: "text"
|
|
defp custom_field_input_type(:integer), do: "number"
|
|
defp custom_field_input_type(:boolean), do: "checkbox"
|
|
defp custom_field_input_type(:date), do: "date"
|
|
defp custom_field_input_type(:email), do: "email"
|
|
defp custom_field_input_type(_), do: "text"
|
|
|
|
# -----------------------------------------------------------------
|
|
# Helper Functions for Form Value Preservation
|
|
# -----------------------------------------------------------------
|
|
|
|
# Helper to extract existing form values to preserve them when only one field changes
|
|
# This ensures custom_field_values and other fields are preserved when only the dropdown changes
|
|
defp get_existing_form_values(form) do
|
|
%{}
|
|
|> extract_form_value(form, :first_name, &to_string/1)
|
|
|> extract_form_value(form, :last_name, &to_string/1)
|
|
|> extract_form_value(form, :email, &to_string/1)
|
|
|> extract_form_value(form, :street, &to_string/1)
|
|
|> extract_form_value(form, :house_number, &to_string/1)
|
|
|> extract_form_value(form, :postal_code, &to_string/1)
|
|
|> extract_form_value(form, :city, &to_string/1)
|
|
|> extract_form_value(form, :join_date, &format_date_value/1)
|
|
|> extract_form_value(form, :exit_date, &format_date_value/1)
|
|
|> extract_form_value(form, :notes, &to_string/1)
|
|
|> extract_form_value(form, :membership_fee_type_id, &to_string/1)
|
|
|> extract_form_value(form, :membership_fee_start_date, &format_date_value/1)
|
|
|> extract_custom_field_values(form)
|
|
end
|
|
|
|
# Helper to extract a single form field value
|
|
defp extract_form_value(acc, form, field, formatter) do
|
|
if form[field] && form[field].value do
|
|
Map.put(acc, to_string(field), formatter.(form[field].value))
|
|
else
|
|
acc
|
|
end
|
|
end
|
|
|
|
# Extracts custom field values from the form structure
|
|
# The form is a Phoenix.HTML.Form with source being AshPhoenix.Form
|
|
# Custom field values are in form.source.params["custom_field_values"] as a map
|
|
defp extract_custom_field_values(acc, form) do
|
|
cfv_params = get_custom_field_values_params(form)
|
|
|
|
if map_size(cfv_params) > 0 do
|
|
custom_field_values = convert_cfv_params_to_list(cfv_params)
|
|
Map.put(acc, "custom_field_values", custom_field_values)
|
|
else
|
|
acc
|
|
end
|
|
end
|
|
|
|
# Gets custom_field_values from form params
|
|
defp get_custom_field_values_params(form) do
|
|
ash_form = form.source
|
|
|
|
if ash_form && Map.has_key?(ash_form, :params) && ash_form.params["custom_field_values"] do
|
|
ash_form.params["custom_field_values"]
|
|
else
|
|
%{}
|
|
end
|
|
end
|
|
|
|
# Converts custom field values map to sorted list
|
|
defp convert_cfv_params_to_list(cfv_params) do
|
|
cfv_params
|
|
|> Map.to_list()
|
|
|> Enum.sort_by(&parse_numeric_key/1)
|
|
|> Enum.map(&build_custom_field_value/1)
|
|
end
|
|
|
|
# Parses numeric key for sorting
|
|
defp parse_numeric_key({key, _}) do
|
|
case Integer.parse(key) do
|
|
{num, _} -> num
|
|
:error -> 999_999
|
|
end
|
|
end
|
|
|
|
# Builds a custom field value map from params
|
|
defp build_custom_field_value({_key, cfv_map}) do
|
|
%{
|
|
"custom_field_id" => Map.get(cfv_map, "custom_field_id", ""),
|
|
"value" => extract_custom_field_value_from_map(Map.get(cfv_map, "value", %{}))
|
|
}
|
|
end
|
|
|
|
# Extracts the value map structure from a custom field value
|
|
# Handles both map format and Ash.Union struct format
|
|
defp extract_custom_field_value_from_map(%Ash.Union{} = union) do
|
|
union_type = Atom.to_string(union.type)
|
|
|
|
%{
|
|
"_union_type" => union_type,
|
|
"type" => union_type,
|
|
"value" => format_custom_field_value(union.value)
|
|
}
|
|
end
|
|
|
|
defp extract_custom_field_value_from_map(value_map) when is_map(value_map) do
|
|
union_type = extract_union_type_from_map(value_map)
|
|
value = Map.get(value_map, "value") || Map.get(value_map, :value)
|
|
|
|
%{
|
|
"_union_type" => union_type,
|
|
"type" => union_type,
|
|
"value" => format_custom_field_value(value)
|
|
}
|
|
end
|
|
|
|
defp extract_custom_field_value_from_map(_),
|
|
do: %{"_union_type" => "", "type" => "", "value" => ""}
|
|
|
|
# Extracts union type from map, checking various possible locations
|
|
defp extract_union_type_from_map(value_map) do
|
|
cond do
|
|
has_non_empty_string(value_map, "_union_type") ->
|
|
Map.get(value_map, "_union_type")
|
|
|
|
has_non_empty_atom(value_map, :_union_type) ->
|
|
to_string(Map.get(value_map, :_union_type))
|
|
|
|
has_atom_type(value_map) ->
|
|
Atom.to_string(Map.get(value_map, :type))
|
|
|
|
has_string_type(value_map) ->
|
|
Map.get(value_map, "type")
|
|
|
|
true ->
|
|
""
|
|
end
|
|
end
|
|
|
|
# Helper to check if map has non-empty string value
|
|
defp has_non_empty_string(map, key) do
|
|
value = Map.get(map, key)
|
|
value && value != ""
|
|
end
|
|
|
|
# Helper to check if map has non-empty atom value
|
|
defp has_non_empty_atom(map, key) do
|
|
value = Map.get(map, key)
|
|
value && value != ""
|
|
end
|
|
|
|
# Helper to check if map has atom type
|
|
defp has_atom_type(map) do
|
|
value = Map.get(map, :type)
|
|
value && is_atom(value)
|
|
end
|
|
|
|
# Helper to check if map has string type
|
|
defp has_string_type(map) do
|
|
value = Map.get(map, "type")
|
|
value && is_binary(value)
|
|
end
|
|
|
|
# Formats custom field value based on its type
|
|
defp format_custom_field_value(%Date{} = date), do: Date.to_iso8601(date)
|
|
defp format_custom_field_value(%Decimal{} = decimal), do: Decimal.to_string(decimal, :normal)
|
|
defp format_custom_field_value(value) when is_boolean(value), do: to_string(value)
|
|
defp format_custom_field_value(value) when is_binary(value), do: value
|
|
defp format_custom_field_value(value), do: to_string(value)
|
|
|
|
# Formats date value (Date or string) to string
|
|
defp format_date_value(%Date{} = date), do: Date.to_iso8601(date)
|
|
defp format_date_value(value) when is_binary(value), do: value
|
|
defp format_date_value(_), do: ""
|
|
end
|