refactor: replace ContributionSettingsLive mockup with MembershipFeeSettingsLive in navigation

This commit is contained in:
Moritz 2025-12-12 18:05:19 +01:00
parent 98b908abdb
commit 96729cb2f4
9 changed files with 76 additions and 334 deletions

View file

@ -144,18 +144,25 @@ defmodule Mv.Membership.Setting do
# Validate default_membership_fee_type_id exists if set
validate fn changeset, _context ->
fee_type_id = Ash.Changeset.get_attribute(changeset, :default_membership_fee_type_id)
fee_type_id =
Ash.Changeset.get_attribute(changeset, :default_membership_fee_type_id)
if fee_type_id do
case Ash.get(Mv.MembershipFees.MembershipFeeType, fee_type_id) do
{:ok, _} -> :ok
{:error, _} ->
{:error, field: :default_membership_fee_type_id, message: "Membership fee type not found"}
end
else
:ok # Optional, can be nil
end
end, on: [:create, :update]
if fee_type_id do
case Ash.get(Mv.MembershipFees.MembershipFeeType, fee_type_id) do
{:ok, _} ->
:ok
{:error, _} ->
{:error,
field: :default_membership_fee_type_id,
message: "Membership fee type not found"}
end
else
# Optional, can be nil
:ok
end
end,
on: [:create, :update]
end
attributes do

View file

@ -59,55 +59,67 @@ defmodule Mv.MembershipFees.MembershipFeeType do
validations do
# Prevent interval changes after creation
validate fn changeset, _context ->
if Ash.Changeset.changing_attribute?(changeset, :interval) do
case changeset.data do
nil -> :ok # Creating new resource, interval can be set
_existing -> {:error, field: :interval, message: "Interval cannot be changed after creation"}
end
else
:ok
end
end, on: [:update]
if Ash.Changeset.changing_attribute?(changeset, :interval) do
case changeset.data do
# Creating new resource, interval can be set
nil ->
:ok
_existing ->
{:error,
field: :interval, message: "Interval cannot be changed after creation"}
end
else
:ok
end
end,
on: [:update]
# Prevent deletion if assigned to members
validate fn changeset, _context ->
if changeset.action_type == :destroy do
require Ash.Query
if changeset.action_type == :destroy do
require Ash.Query
member_count =
Mv.Membership.Member
|> Ash.Query.filter(membership_fee_type_id == ^changeset.data.id)
|> Ash.count!()
member_count =
Mv.Membership.Member
|> Ash.Query.filter(membership_fee_type_id == ^changeset.data.id)
|> Ash.count!()
if member_count > 0 do
{:error, message: "Cannot delete membership fee type: #{member_count} member(s) are assigned to it"}
else
:ok
end
else
:ok
end
end, on: [:destroy]
if member_count > 0 do
{:error,
message:
"Cannot delete membership fee type: #{member_count} member(s) are assigned to it"}
else
:ok
end
else
:ok
end
end,
on: [:destroy]
# Prevent deletion if cycles exist
validate fn changeset, _context ->
if changeset.action_type == :destroy do
require Ash.Query
if changeset.action_type == :destroy do
require Ash.Query
cycle_count =
Mv.MembershipFees.MembershipFeeCycle
|> Ash.Query.filter(membership_fee_type_id == ^changeset.data.id)
|> Ash.count!()
cycle_count =
Mv.MembershipFees.MembershipFeeCycle
|> Ash.Query.filter(membership_fee_type_id == ^changeset.data.id)
|> Ash.count!()
if cycle_count > 0 do
{:error, message: "Cannot delete membership fee type: #{cycle_count} cycle(s) reference it"}
else
:ok
end
else
:ok
end
end, on: [:destroy]
if cycle_count > 0 do
{:error,
message:
"Cannot delete membership fee type: #{cycle_count} cycle(s) reference it"}
else
:ok
end
else
:ok
end
end,
on: [:destroy]
end
attributes do

View file

@ -31,7 +31,9 @@ defmodule MvWeb.Layouts.Navbar do
<ul class="bg-base-200 rounded-t-none p-2 z-10 w-48">
<li><.link navigate="/contribution_types">{gettext("Contribution Types")}</.link></li>
<li>
<.link navigate="/contribution_settings">{gettext("Contribution Settings")}</.link>
<.link navigate="/membership_fee_settings">
{gettext("Membership Fee Settings")}
</.link>
</li>
</ul>
</details>

View file

@ -43,7 +43,7 @@ defmodule MvWeb.ContributionPeriodLive.Show do
· {gettext("Member since")}: <span class="font-mono">{@member.joined_at}</span>
</:subtitle>
<:actions>
<.link navigate={~p"/contribution_settings"} class="btn btn-ghost btn-sm">
<.link navigate={~p"/membership_fee_settings"} class="btn btn-ghost btn-sm">
<.icon name="hero-arrow-left" class="size-4" />
{gettext("Back to Settings")}
</.link>

View file

@ -1,277 +0,0 @@
defmodule MvWeb.ContributionSettingsLive do
@moduledoc """
Mock-up LiveView for Contribution Settings (Admin).
This is a preview-only page that displays the planned UI for managing
global contribution settings. It shows static mock data and is not functional.
## Planned Features (Future Implementation)
- Set default contribution type for new members
- Configure whether joining period is included in contributions
- Explanatory text with examples
## Settings
- `default_contribution_type_id` - UUID of the default contribution type
- `include_joining_period` - Boolean whether to include joining period
## Note
This page is intentionally non-functional and serves as a UI mockup
for the upcoming Membership Contributions feature.
"""
use MvWeb, :live_view
@impl true
def mount(_params, _session, socket) do
{:ok,
socket
|> assign(:page_title, gettext("Contribution Settings"))
|> assign(:contribution_types, mock_contribution_types())
|> assign(:selected_type_id, "1")
|> assign(:include_joining_period, true)}
end
@impl true
def render(assigns) do
~H"""
<Layouts.app flash={@flash} current_user={@current_user}>
<.mockup_warning />
<.header>
{gettext("Contribution Settings")}
<:subtitle>
{gettext("Configure global settings for membership contributions.")}
</: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 class="space-y-6">
<%!-- Default Contribution Type --%>
<fieldset class="fieldset">
<label class="label">
<span class="label-text font-semibold">
{gettext("Default Contribution Type")}
</span>
</label>
<select class="select select-bordered w-full" disabled>
<option :for={ct <- @contribution_types} selected={ct.id == @selected_type_id}>
{ct.name} ({format_currency(ct.amount)}, {format_interval(ct.interval)})
</option>
</select>
<p class="text-sm text-base-content/60 mt-2">
{gettext(
"This contribution type is automatically assigned to all new members. Can be changed individually per member."
)}
</p>
</fieldset>
<%!-- Include Joining Period --%>
<fieldset class="fieldset">
<label class="label cursor-pointer justify-start gap-3">
<input
type="checkbox"
class="checkbox checkbox-primary"
checked={@include_joining_period}
disabled
/>
<span class="label-text font-semibold">
{gettext("Include joining period")}
</span>
</label>
<div class="ml-9 space-y-2">
<p class="text-sm text-base-content/60">
{gettext("When active: Members pay from the period of their joining.")}
</p>
<p class="text-sm text-base-content/60">
{gettext("When inactive: Members pay from the next full period after joining.")}
</p>
</div>
</fieldset>
<div class="divider"></div>
<button type="button" class="btn btn-primary w-full" disabled>
<.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 Period 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 Period 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 Period 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 Period 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>
<.example_member_card />
</Layouts.app>
"""
end
# Example member card with link to period view
defp example_member_card(assigns) do
~H"""
<div class="card bg-base-100 shadow-xl mt-6">
<div class="card-body">
<h2 class="card-title">
<.icon name="hero-user" class="size-5" />
{gettext("Example: Member Contribution View")}
</h2>
<p class="text-base-content/70">
{gettext(
"See how the contribution periods will be displayed for an individual member. This example shows Maria Weber with multiple contribution periods."
)}
</p>
<div class="card-actions justify-end">
<.link navigate={~p"/contributions/member/example"} class="btn btn-primary btn-sm">
<.icon name="hero-eye" class="size-4" />
{gettext("View Example Member")}
</.link>
</div>
</div>
</div>
"""
end
# Mock-up warning banner component - subtle orange style
defp mockup_warning(assigns) do
~H"""
<div class="border border-warning text-warning bg-base-100 rounded-lg px-4 py-3 mb-6 flex items-center gap-3">
<.icon name="hero-exclamation-triangle" class="size-5 shrink-0" />
<div>
<span class="font-semibold">{gettext("Preview Mockup")}</span>
<span class="text-sm text-base-content/70 ml-2">
{gettext("This page is not functional and only displays the planned features.")}
</span>
</div>
</div>
"""
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/60">{gettext("Joining date")}:</span>
<span class="font-mono">{@joining_date}</span>
</p>
<p>
<span class="text-base-content/60">{gettext("Contribution start")}:</span>
<span class="font-mono font-semibold text-primary">{@start_date}</span>
</p>
<p>
<span class="text-base-content/60">{gettext("Generated periods")}:</span>
<span class="font-mono">
{Enum.join(@periods, ", ")}
</span>
</p>
</div>
<p class="text-xs text-base-content/60 italic"> {@note}</p>
</div>
"""
end
# Mock data for demonstration
defp mock_contribution_types do
[
%{
id: "1",
name: gettext("Regular"),
amount: Decimal.new("60.00"),
interval: :yearly
},
%{
id: "2",
name: gettext("Reduced"),
amount: Decimal.new("30.00"),
interval: :yearly
},
%{
id: "3",
name: gettext("Student"),
amount: Decimal.new("5.00"),
interval: :monthly
},
%{
id: "4",
name: gettext("Family"),
amount: Decimal.new("25.00"),
interval: :quarterly
}
]
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")
end

View file

@ -103,7 +103,9 @@ defmodule MvWeb.MembershipFeeSettingsLive do
value={fee_type.id}
selected={fee_type.id == @selected_fee_type_id}
>
{fee_type.name} ({format_currency(fee_type.amount)}, {format_interval(fee_type.interval)})
{fee_type.name} ({format_currency(fee_type.amount)}, {format_interval(
fee_type.interval
)})
</option>
</select>
<p class="text-sm text-base-content/60 mt-2">
@ -281,4 +283,3 @@ defmodule MvWeb.MembershipFeeSettingsLive do
|> Ash.update()
end
end

View file

@ -74,7 +74,6 @@ defmodule MvWeb.Router do
# Contribution Management (Mock-ups)
live "/contribution_types", ContributionTypeLive.Index, :index
live "/contribution_settings", ContributionSettingsLive
live "/contributions/member/:id", ContributionPeriodLive.Show, :show
post "/set_locale", LocaleController, :set_locale

View file

@ -96,4 +96,3 @@ defmodule Mv.Membership.MembershipFeeSettingsTest do
defp error_on_field?(_, _), do: false
end

View file

@ -203,4 +203,3 @@ defmodule Mv.MembershipFees.MembershipFeeTypeIntegrationTest do
defp extract_error_message(_), do: ""
end