defmodule MvWeb.MembershipFeeSettingsLive do @moduledoc """ LiveView for managing membership fee settings (Admin). Allows administrators to configure: - Default membership fee type for new members - Whether to include the joining cycle in membership fee generation """ use MvWeb, :live_view alias Mv.Membership alias Mv.MembershipFees.MembershipFeeType @impl true def mount(_params, _session, socket) do {:ok, settings} = Membership.get_settings() membership_fee_types = MembershipFeeType |> Ash.Query.sort(name: :asc) |> Ash.read!() {:ok, socket |> 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))} end @impl true def handle_event("validate", %{"settings" => params}, socket) do changeset = %{} |> validate_settings(params) |> to_form(as: :settings) {:noreply, assign(socket, changeset: changeset)} end def handle_event("save", %{"settings" => params}, socket) do case update_settings(socket.assigns.settings, 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))} {:error, changeset} -> {:noreply, socket |> put_flash(:error, gettext("Failed to save settings. Please check the errors below.")) |> assign(:changeset, to_form(changeset, as: :settings))} end end @impl true def render(assigns) do ~H""" <.header> {gettext("Membership Fee Settings")} <:subtitle> {gettext("Configure global settings for membership fees.")}
<%!-- Settings Form --%>

<.icon name="hero-cog-6-tooth" class="size-5" /> {gettext("Global Settings")}

<.form for={@changeset} phx-change="validate" phx-submit="save" class="space-y-6" > <%!-- Default Membership Fee Type --%>

{gettext( "This membership fee type is automatically assigned to all new members. Can be changed individually per member." )}

<%!-- Include Joining Cycle --%>

{gettext("When active: Members pay from the cycle of their joining.")}

{gettext("When inactive: Members pay from the next full cycle after joining.")}

<%!-- Examples Card --%>

<.icon name="hero-light-bulb" class="size-5" /> {gettext("Examples")}

<.example_section title={gettext("Yearly Interval - Joining Cycle Included")} joining_date="15.03.2023" include_joining={true} start_date="01.01.2023" periods={["2023", "2024", "2025"]} note={gettext("Member pays for the year they joined")} />
<.example_section title={gettext("Yearly Interval - Joining Cycle Excluded")} joining_date="15.03.2023" include_joining={false} start_date="01.01.2024" periods={["2024", "2025"]} note={gettext("Member pays from the next full year")} />
<.example_section title={gettext("Quarterly Interval - Joining Cycle Excluded")} joining_date="15.05.2024" include_joining={false} start_date="01.07.2024" periods={["Q3/2024", "Q4/2024", "Q1/2025"]} note={gettext("Member pays from the next full quarter")} />
<.example_section title={gettext("Monthly Interval - Joining Cycle Included")} joining_date="15.03.2024" include_joining={true} start_date="01.03.2024" periods={["03/2024", "04/2024", "05/2024", "..."]} note={gettext("Member pays from the joining month")} />
""" end # Example section component attr :title, :string, required: true attr :joining_date, :string, required: true attr :include_joining, :boolean, required: true attr :start_date, :string, required: true attr :periods, :list, required: true attr :note, :string, required: true defp example_section(assigns) do ~H"""

{@title}

{gettext("Joining date")}: {@joining_date}

{gettext("Membership fee start")}: {@start_date}

{gettext("Generated cycles")}: {Enum.join(@periods, ", ")}

→ {@note}

""" end defp format_currency(%Decimal{} = amount) do "#{Decimal.to_string(amount)} €" end defp format_interval(:monthly), do: gettext("Monthly") defp format_interval(:quarterly), do: gettext("Quarterly") 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 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() end end