mitgliederverwaltung/lib/mv_web/live/membership_fee_settings_live.ex

261 lines
9 KiB
Elixir

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_form()}
end
@impl true
def handle_event("validate", %{"settings" => params}, socket) do
{:noreply, assign(socket, form: AshPhoenix.Form.validate(socket.assigns.form, params))}
end
def handle_event("save", %{"settings" => params}, socket) do
case AshPhoenix.Form.submit(socket.assigns.form, params: params) do
{:ok, updated_settings} ->
{:noreply,
socket
|> assign(:settings, updated_settings)
|> put_flash(:info, gettext("Settings saved successfully."))
|> assign_form()}
{:error, form} ->
{:noreply, assign(socket, form: form)}
end
end
@impl true
def render(assigns) do
~H"""
<Layouts.app flash={@flash} current_user={@current_user}>
<.header>
{gettext("Membership Fee Settings")}
<:subtitle>
{gettext("Configure global settings for membership fees.")}
</:subtitle>
</.header>
<div class="grid gap-6 lg:grid-cols-2">
<%!-- Settings Form --%>
<div class="card bg-base-100 shadow-xl">
<div class="card-body">
<h2 class="card-title">
<.icon name="hero-cog-6-tooth" class="size-5" />
{gettext("Global Settings")}
</h2>
<.form
for={@form}
phx-change="validate"
phx-submit="save"
class="space-y-6"
>
<%!-- Default Membership Fee Type --%>
<fieldset class="fieldset">
<label for="default_membership_fee_type_id" class="label">
<span class="label-text font-semibold">
{gettext("Default Membership Fee Type")}
</span>
</label>
<select
id="default_membership_fee_type_id"
name="settings[default_membership_fee_type_id]"
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")}
>
<option value="">{gettext("None (no default)")}</option>
<option
:for={fee_type <- @membership_fee_types}
value={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."
)}
</p>
</fieldset>
<%!-- Include Joining Cycle --%>
<fieldset class="fieldset">
<label class="label cursor-pointer justify-start gap-3">
<input
type="checkbox"
name="settings[include_joining_cycle]"
class="checkbox checkbox-primary"
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.")}
</p>
<p class="text-sm text-base-content/60">
{gettext("When inactive: Members pay from the next full cycle after joining.")}
</p>
</div>
</fieldset>
<div class="divider"></div>
<button type="submit" class="btn btn-primary w-full">
<.icon name="hero-check" class="size-5" />
{gettext("Save Settings")}
</button>
</.form>
</div>
</div>
<%!-- Examples Card --%>
<div class="card bg-base-200">
<div class="card-body">
<h2 class="card-title">
<.icon name="hero-light-bulb" class="size-5" />
{gettext("Examples")}
</h2>
<.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")}
/>
<div class="divider"></div>
<.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")}
/>
<div class="divider"></div>
<.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")}
/>
<div class="divider"></div>
<.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")}
/>
</div>
</div>
</div>
</Layouts.app>
"""
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"""
<div class="space-y-2">
<h3 class="font-semibold text-sm">{@title}</h3>
<div class="bg-base-300 rounded-lg p-3 text-sm space-y-1">
<p>
<span class="text-base-content/80">{gettext("Joining date")}:</span>
<span class="font-mono">{@joining_date}</span>
</p>
<p>
<span class="text-base-content/80">{gettext("Membership fee start")}:</span>
<span class="font-mono font-semibold text-base-content">{@start_date}</span>
</p>
<p>
<span class="text-base-content/80">{gettext("Generated cycles")}:</span>
<span class="font-mono">
{Enum.join(@periods, ", ")}
</span>
</p>
</div>
<p class="text-xs text-base-content/80 italic">→ {@note}</p>
</div>
"""
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 assign_form(%{assigns: %{settings: settings}} = socket) do
form =
AshPhoenix.Form.for_update(
settings,
:update_membership_fee_settings,
api: Membership,
as: "settings",
forms: [auto?: true]
)
assign(socket, form: to_form(form))
end
end