refactor: migrate MembershipFeeSettingsLive to AshPhoenix.Form

This commit is contained in:
Moritz 2025-12-12 19:05:41 +01:00
parent 9e441213be
commit 8cbd481709
3 changed files with 51 additions and 56 deletions

View file

@ -95,6 +95,8 @@ defmodule Mv.Membership.Setting do
description "Updates the membership fee configuration"
require_atomic? false
accept [:include_joining_cycle, :default_membership_fee_type_id]
change Mv.Membership.Setting.Changes.NormalizeDefaultFeeTypeId
end
end

View file

@ -0,0 +1,19 @@
defmodule Mv.Membership.Setting.Changes.NormalizeDefaultFeeTypeId do
@moduledoc """
Ash change that normalizes empty strings to nil for default_membership_fee_type_id.
HTML forms submit empty select values as empty strings (""), but the database
expects nil for optional UUID fields. This change converts "" to nil.
"""
use Ash.Resource.Change
def change(changeset, _opts, _context) do
default_fee_type_id = Ash.Changeset.get_attribute(changeset, :default_membership_fee_type_id)
if default_fee_type_id == "" do
Ash.Changeset.force_change_attribute(changeset, :default_membership_fee_type_id, nil)
else
changeset
end
end
end

View file

@ -25,37 +25,25 @@ defmodule MvWeb.MembershipFeeSettingsLive do
|> assign(:page_title, gettext("Membership Fee Settings"))
|> assign(:settings, settings)
|> assign(:membership_fee_types, membership_fee_types)
|> assign(:selected_fee_type_id, settings.default_membership_fee_type_id)
|> assign(:include_joining_cycle, settings.include_joining_cycle)
|> assign(:changeset, to_form(%{}, as: :settings))}
|> assign_form()}
end
@impl true
def handle_event("validate", %{"settings" => params}, socket) do
changeset =
%{}
|> validate_settings(params)
|> to_form(as: :settings)
{:noreply, assign(socket, changeset: changeset)}
{:noreply, assign(socket, form: AshPhoenix.Form.validate(socket.assigns.form, params))}
end
def handle_event("save", %{"settings" => params}, socket) do
case update_settings(socket.assigns.settings, params) do
case AshPhoenix.Form.submit(socket.assigns.form, params: params) do
{:ok, updated_settings} ->
{:noreply,
socket
|> put_flash(:info, gettext("Settings saved successfully."))
|> assign(:settings, updated_settings)
|> assign(:selected_fee_type_id, updated_settings.default_membership_fee_type_id)
|> assign(:include_joining_cycle, updated_settings.include_joining_cycle)
|> assign(:changeset, to_form(%{}, as: :settings))}
|> put_flash(:info, gettext("Settings saved successfully."))
|> assign_form()}
{:error, changeset} ->
{:noreply,
socket
|> put_flash(:error, gettext("Failed to save settings. Please check the errors below."))
|> assign(:changeset, to_form(changeset, as: :settings))}
{:error, form} ->
{:noreply, assign(socket, form: form)}
end
end
@ -80,7 +68,7 @@ defmodule MvWeb.MembershipFeeSettingsLive do
</h2>
<.form
for={@changeset}
for={@form}
phx-change="validate"
phx-submit="save"
class="space-y-6"
@ -95,7 +83,10 @@ defmodule MvWeb.MembershipFeeSettingsLive do
<select
id="default_membership_fee_type_id"
name="settings[default_membership_fee_type_id]"
class="select select-bordered w-full"
class={[
"select select-bordered w-full",
if(@form.errors[:default_membership_fee_type_id], do: "select-error", else: "")
]}
phx-debounce="blur"
aria-label={gettext("Default Membership Fee Type")}
>
@ -103,13 +94,16 @@ defmodule MvWeb.MembershipFeeSettingsLive do
<option
:for={fee_type <- @membership_fee_types}
value={fee_type.id}
selected={fee_type.id == @selected_fee_type_id}
selected={fee_type.id == @form[:default_membership_fee_type_id].value}
>
{fee_type.name} ({format_currency(fee_type.amount)}, {format_interval(
fee_type.interval
)})
</option>
</select>
<%= for {msg, _opts} <- @form.errors[:default_membership_fee_type_id] || [] do %>
<p class="text-error text-sm mt-1">{msg}</p>
<% end %>
<p class="text-sm text-base-content/60 mt-2">
{gettext(
"This membership fee type is automatically assigned to all new members. Can be changed individually per member."
@ -124,13 +118,16 @@ defmodule MvWeb.MembershipFeeSettingsLive do
type="checkbox"
name="settings[include_joining_cycle]"
class="checkbox checkbox-primary"
checked={@include_joining_cycle}
checked={@form[:include_joining_cycle].value}
phx-debounce="blur"
/>
<span class="label-text font-semibold">
{gettext("Include joining cycle")}
</span>
</label>
<%= for {msg, _opts} <- @form.errors[:include_joining_cycle] || [] do %>
<p class="text-error text-sm ml-9 mt-1">{msg}</p>
<% end %>
<div class="ml-9 space-y-2">
<p class="text-sm text-base-content/60">
{gettext("When active: Members pay from the cycle of their joining.")}
@ -249,39 +246,16 @@ defmodule MvWeb.MembershipFeeSettingsLive do
defp format_interval(:half_yearly), do: gettext("Half-yearly")
defp format_interval(:yearly), do: gettext("Yearly")
defp validate_settings(attrs, params) do
attrs
|> Map.merge(params)
|> validate_default_fee_type()
end
defp assign_form(%{assigns: %{settings: settings}} = socket) do
form =
AshPhoenix.Form.for_update(
settings,
:update_membership_fee_settings,
api: Membership,
as: "settings",
forms: [auto?: true]
)
defp validate_default_fee_type(%{"default_membership_fee_type_id" => ""} = attrs) do
Map.put(attrs, "default_membership_fee_type_id", nil)
end
defp validate_default_fee_type(attrs), do: attrs
defp update_settings(settings, params) do
# Convert empty string to nil for optional field
params =
if params["default_membership_fee_type_id"] == "" do
Map.put(params, "default_membership_fee_type_id", nil)
else
params
end
# Convert checkbox value to boolean
params =
Map.update(params, "include_joining_cycle", false, fn
"true" -> true
"false" -> false
true -> true
false -> false
_ -> false
end)
settings
|> Ash.Changeset.for_update(:update_membership_fee_settings, params)
|> Ash.update()
assign(socket, form: to_form(form))
end
end