All checks were successful
continuous-integration/drone/push Build is passing
Complete refactoring of resources, database tables, code references, tests, and documentation for improved naming consistency.
218 lines
7.2 KiB
Elixir
218 lines
7.2 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
|
|
- Manage custom properties (dynamic fields)
|
|
- Real-time validation with visual feedback
|
|
- Link/unlink user accounts
|
|
|
|
## Form Fields
|
|
**Required:**
|
|
- first_name, last_name, email
|
|
|
|
**Optional:**
|
|
- birth_date, phone_number, address fields (city, street, house_number, postal_code)
|
|
- join_date, exit_date
|
|
- paid status
|
|
- notes
|
|
|
|
## Custom Field Values
|
|
Members can have dynamic custom field values defined by CustomFields.
|
|
The form dynamically renders inputs based on available CustomFields.
|
|
|
|
## Events
|
|
- `validate` - Real-time form validation
|
|
- `save` - Submit form (create or update member)
|
|
- Custom field value management events for adding/removing custom fields
|
|
"""
|
|
use MvWeb, :live_view
|
|
|
|
@impl true
|
|
def render(assigns) do
|
|
~H"""
|
|
<Layouts.app flash={@flash} current_user={@current_user}>
|
|
<.header>
|
|
{@page_title}
|
|
<:subtitle>
|
|
{gettext("Use this form to manage member records and their properties.")}
|
|
</:subtitle>
|
|
</.header>
|
|
|
|
<.form for={@form} id="member-form" phx-change="validate" phx-submit="save">
|
|
<.input field={@form[:first_name]} label={gettext("First Name")} required />
|
|
<.input field={@form[:last_name]} label={gettext("Last Name")} required />
|
|
<.input field={@form[:email]} label={gettext("Email")} required type="email" />
|
|
<.input field={@form[:birth_date]} label={gettext("Birth Date")} type="date" />
|
|
<.input field={@form[:paid]} label={gettext("Paid")} type="checkbox" />
|
|
<.input field={@form[:phone_number]} label={gettext("Phone Number")} />
|
|
<.input field={@form[:join_date]} label={gettext("Join Date")} type="date" />
|
|
<.input field={@form[:exit_date]} label={gettext("Exit Date")} type="date" />
|
|
<.input field={@form[:notes]} label={gettext("Notes")} />
|
|
<.input field={@form[:city]} label={gettext("City")} />
|
|
<.input field={@form[:street]} label={gettext("Street")} />
|
|
<.input field={@form[:house_number]} label={gettext("House Number")} />
|
|
<.input field={@form[:postal_code]} label={gettext("Postal Code")} />
|
|
|
|
<h3 class="mt-8 mb-2 text-lg font-semibold">{gettext("Custom Field Values")}</h3>
|
|
<.inputs_for :let={f_custom_field_value} field={@form[:custom_field_values]}>
|
|
<% type =
|
|
Enum.find(@custom_fields, &(&1.id == f_custom_field_value[:custom_field_id].value)) %>
|
|
<.inputs_for :let={value_form} field={f_custom_field_value[:value]}>
|
|
<% input_type =
|
|
cond do
|
|
type && type.value_type == :boolean -> "checkbox"
|
|
type && type.value_type == :date -> :date
|
|
true -> :text
|
|
end %>
|
|
<.input field={value_form[:value]} label={type && type.name} type={input_type} />
|
|
</.inputs_for>
|
|
<input
|
|
type="hidden"
|
|
name={f_custom_field_value[:custom_field_id].name}
|
|
value={f_custom_field_value[:custom_field_id].value}
|
|
/>
|
|
</.inputs_for>
|
|
|
|
<.button phx-disable-with={gettext("Saving...")} variant="primary">
|
|
{gettext("Save Member")}
|
|
</.button>
|
|
<.button navigate={return_path(@return_to, @member)}>{gettext("Cancel")}</.button>
|
|
</.form>
|
|
</Layouts.app>
|
|
"""
|
|
end
|
|
|
|
@impl true
|
|
def mount(params, _session, socket) do
|
|
{: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)
|
|
end
|
|
|
|
action = if is_nil(member), do: "New", else: "Edit"
|
|
page_title = action <> " " <> "Member"
|
|
|
|
{: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_form()}
|
|
end
|
|
|
|
defp return_to("show"), do: "show"
|
|
defp return_to(_), do: "index"
|
|
|
|
@impl true
|
|
def handle_event("validate", %{"member" => member_params}, socket) do
|
|
{:noreply, assign(socket, form: AshPhoenix.Form.validate(socket.assigns.form, member_params))}
|
|
end
|
|
|
|
def handle_event("save", %{"member" => member_params}, socket) do
|
|
case AshPhoenix.Form.submit(socket.assigns.form, params: member_params) do
|
|
{:ok, member} ->
|
|
notify_parent({:saved, member})
|
|
|
|
action =
|
|
case socket.assigns.form.source.type do
|
|
:create -> gettext("create")
|
|
:update -> gettext("update")
|
|
other -> to_string(other)
|
|
end
|
|
|
|
socket =
|
|
socket
|
|
|> put_flash(:info, gettext("Member %{action} successfully", action: action))
|
|
|> push_navigate(to: return_path(socket.assigns.return_to, member))
|
|
|
|
{:noreply, socket}
|
|
|
|
{:error, form} ->
|
|
{:noreply, assign(socket, form: form)}
|
|
end
|
|
end
|
|
|
|
defp notify_parent(msg), do: send(self(), {__MODULE__, msg})
|
|
|
|
defp assign_form(%{assigns: %{member: member}} = socket) do
|
|
form =
|
|
if member do
|
|
{:ok, member} = Ash.load(member, custom_field_values: [:custom_field])
|
|
|
|
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]
|
|
)
|
|
|
|
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]
|
|
)
|
|
end
|
|
|
|
assign(socket, form: to_form(form))
|
|
end
|
|
|
|
defp return_path("index", _member), do: ~p"/members"
|
|
defp return_path("show", member), do: ~p"/members/#{member.id}"
|
|
end
|