Cycle Management & Member Integration closes #279 #294

Open
moritz wants to merge 48 commits from feature/279_cycle_management into main
3 changed files with 51 additions and 56 deletions
Showing only changes of commit 8cbd481709 - Show all commits

View file

@ -95,6 +95,8 @@ defmodule Mv.Membership.Setting do
description "Updates the membership fee configuration" description "Updates the membership fee configuration"
require_atomic? false require_atomic? false
accept [:include_joining_cycle, :default_membership_fee_type_id] accept [:include_joining_cycle, :default_membership_fee_type_id]
change Mv.Membership.Setting.Changes.NormalizeDefaultFeeTypeId
end end
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(:page_title, gettext("Membership Fee Settings"))
|> assign(:settings, settings) |> assign(:settings, settings)
|> assign(:membership_fee_types, membership_fee_types) |> assign(:membership_fee_types, membership_fee_types)
|> assign(:selected_fee_type_id, settings.default_membership_fee_type_id) |> assign_form()}
|> assign(:include_joining_cycle, settings.include_joining_cycle)
|> assign(:changeset, to_form(%{}, as: :settings))}
end end
@impl true @impl true
def handle_event("validate", %{"settings" => params}, socket) do def handle_event("validate", %{"settings" => params}, socket) do
changeset = {:noreply, assign(socket, form: AshPhoenix.Form.validate(socket.assigns.form, params))}
%{}
|> validate_settings(params)
|> to_form(as: :settings)
{:noreply, assign(socket, changeset: changeset)}
end end
def handle_event("save", %{"settings" => params}, socket) do 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} -> {:ok, updated_settings} ->
{:noreply, {:noreply,
socket socket
|> put_flash(:info, gettext("Settings saved successfully."))
|> assign(:settings, updated_settings) |> assign(:settings, updated_settings)
|> assign(:selected_fee_type_id, updated_settings.default_membership_fee_type_id) |> put_flash(:info, gettext("Settings saved successfully."))
|> assign(:include_joining_cycle, updated_settings.include_joining_cycle) |> assign_form()}
|> assign(:changeset, to_form(%{}, as: :settings))}
{:error, changeset} -> {:error, form} ->
{:noreply, {:noreply, assign(socket, form: form)}
socket
|> put_flash(:error, gettext("Failed to save settings. Please check the errors below."))
|> assign(:changeset, to_form(changeset, as: :settings))}
end end
end end
@ -80,7 +68,7 @@ defmodule MvWeb.MembershipFeeSettingsLive do
</h2> </h2>
<.form <.form
for={@changeset} for={@form}
phx-change="validate" phx-change="validate"
phx-submit="save" phx-submit="save"
class="space-y-6" class="space-y-6"
@ -95,7 +83,10 @@ defmodule MvWeb.MembershipFeeSettingsLive do
<select <select
id="default_membership_fee_type_id" id="default_membership_fee_type_id"
name="settings[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" phx-debounce="blur"
aria-label={gettext("Default Membership Fee Type")} aria-label={gettext("Default Membership Fee Type")}
> >
@ -103,13 +94,16 @@ defmodule MvWeb.MembershipFeeSettingsLive do
<option <option
:for={fee_type <- @membership_fee_types} :for={fee_type <- @membership_fee_types}
value={fee_type.id} 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.name} ({format_currency(fee_type.amount)}, {format_interval(
fee_type.interval fee_type.interval
)}) )})
</option> </option>
</select> </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"> <p class="text-sm text-base-content/60 mt-2">
{gettext( {gettext(
"This membership fee type is automatically assigned to all new members. Can be changed individually per member." "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" type="checkbox"
name="settings[include_joining_cycle]" name="settings[include_joining_cycle]"
class="checkbox checkbox-primary" class="checkbox checkbox-primary"
checked={@include_joining_cycle} checked={@form[:include_joining_cycle].value}
phx-debounce="blur" phx-debounce="blur"
/> />
<span class="label-text font-semibold"> <span class="label-text font-semibold">
{gettext("Include joining cycle")} {gettext("Include joining cycle")}
</span> </span>
</label> </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"> <div class="ml-9 space-y-2">
<p class="text-sm text-base-content/60"> <p class="text-sm text-base-content/60">
{gettext("When active: Members pay from the cycle of their joining.")} {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(:half_yearly), do: gettext("Half-yearly")
defp format_interval(:yearly), do: gettext("Yearly") defp format_interval(:yearly), do: gettext("Yearly")
defp validate_settings(attrs, params) do defp assign_form(%{assigns: %{settings: settings}} = socket) do
attrs form =
|> Map.merge(params) AshPhoenix.Form.for_update(
|> validate_default_fee_type() settings,
end :update_membership_fee_settings,
api: Membership,
as: "settings",
forms: [auto?: true]
)
defp validate_default_fee_type(%{"default_membership_fee_type_id" => ""} = attrs) do assign(socket, form: to_form(form))
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()
end end
end end