Membership fee settings: row-click table, compact default layout

This commit is contained in:
Moritz 2026-03-04 14:50:31 +01:00
parent 60d3fa74fb
commit f9d6936274
Signed by: moritz
GPG key ID: 1020A035E5DD0824
2 changed files with 180 additions and 199 deletions

View file

@ -151,24 +151,22 @@ defmodule MvWeb.MembershipFeeSettingsLive do
</:actions>
</.header>
<div class="grid gap-6 lg:grid-cols-2">
<%!-- Settings Form --%>
<%!-- One card: default setting + fee types table --%>
<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>
<div class="card-body space-y-6">
<%!-- Default setting: one row, clear section title and split hints --%>
<.form
for={@form}
phx-change="validate"
phx-submit="save"
class="space-y-6"
class="space-y-2"
>
<%!-- Default Membership Fee Type --%>
<fieldset class="fieldset">
<label for="default_membership_fee_type_id" class="label">
<h2 class="text-base font-semibold text-base-content">
{gettext("Default settings")}
</h2>
<div class="flex flex-wrap items-end gap-6">
<fieldset class="fieldset flex-1 min-w-[200px] max-w-md">
<label for="default_membership_fee_type_id" class="label py-0">
<span class="label-text font-semibold">
{gettext("Default Membership Fee Type")}
</span>
@ -177,7 +175,7 @@ defmodule MvWeb.MembershipFeeSettingsLive do
id="default_membership_fee_type_id"
name="settings[default_membership_fee_type_id]"
class={[
"select select-bordered",
"select select-bordered w-full",
if(@form.errors[:default_membership_fee_type_id], do: "select-error", else: "")
]}
phx-debounce="blur"
@ -200,16 +198,10 @@ defmodule MvWeb.MembershipFeeSettingsLive do
<p class="text-error text-sm mt-1">{msg}</p>
<% end %>
<% 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">
<fieldset class="fieldset flex-shrink-0">
<label class="label cursor-pointer justify-start gap-3 py-0 min-h-0">
<input
type="checkbox"
name="settings[include_joining_cycle]"
@ -224,31 +216,131 @@ defmodule MvWeb.MembershipFeeSettingsLive do
<%= if @form.errors[:include_joining_cycle] do %>
<%= for error <- List.wrap(@form.errors[:include_joining_cycle]) do %>
<% {msg, _opts} = if is_tuple(error), do: error, else: {error, []} %>
<p class="text-error text-sm ml-9 mt-1">{msg}</p>
<p class="text-error text-sm mt-1">{msg}</p>
<% end %>
<% 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" variant="primary" class="w-full">
<div class="flex-shrink-0 ml-auto border-l border-base-300 pl-6">
<.button type="submit" variant="primary">
<.icon name="hero-check" class="size-5" />
{gettext("Save Settings")}
</.button>
</.form>
</div>
</div>
<%!-- Examples Card (collapsible) --%>
<div class="card bg-base-200">
<ul class="text-sm text-base-content/60 list-disc list-inside space-y-0.5">
<li>{gettext("Default type: Assigned to new members; can be changed per member.")}</li>
<li>
{gettext(
"Include joining cycle: When active, members pay from their joining cycle; when inactive, from the next full cycle."
)}
</li>
</ul>
</.form>
<div class="divider"></div>
<%!-- Fee types table: row click opens edit --%>
<h2 class="text-lg font-semibold">{gettext("Membership Fee Types")}</h2>
<.table
id="membership_fee_types"
rows={@membership_fee_types}
row_id={fn mft -> "mft-#{mft.id}" end}
row_click={
fn mft ->
Phoenix.LiveView.JS.navigate(~p"/membership_fee_settings/#{mft.id}/edit_fee_type")
end
}
row_tooltip={gettext("Click to edit membership fee type")}
>
<:col :let={mft} label={gettext("Name")}>
<span class="font-medium">{mft.name}</span>
<p :if={mft.description} class="text-sm text-base-content/70">{mft.description}</p>
</:col>
<:col :let={mft} label={gettext("Amount")}>
<span class="font-mono">{MembershipFeeHelpers.format_currency(mft.amount)}</span>
</:col>
<:col :let={mft} label={gettext("Interval")}>
<.badge variant="neutral" style="outline">
{MembershipFeeHelpers.format_interval(mft.interval)}
</.badge>
</:col>
<:col :let={mft} label={gettext("Members")}>
<span class="text-sm">{get_member_count(mft, @member_counts)}</span>
</:col>
<:action :let={mft}>
<.tooltip
:if={get_member_count(mft, @member_counts) > 0}
content={
gettext("Cannot delete - %{count} member(s) assigned",
count: get_member_count(mft, @member_counts)
)
}
position="left"
>
<button
phx-click="delete"
phx-value-id={mft.id}
data-confirm={gettext("Are you sure?")}
class="btn btn-ghost btn-xs text-error opacity-50 cursor-not-allowed"
aria-label={
gettext("Cannot delete - %{count} member(s) assigned",
count: get_member_count(mft, @member_counts)
)
}
disabled={true}
>
<.icon name="hero-trash" class="size-4" />
</button>
</.tooltip>
<.button
:if={get_member_count(mft, @member_counts) == 0}
variant="danger"
size="sm"
phx-click="delete"
phx-value-id={mft.id}
data-confirm={gettext("Are you sure?")}
aria-label={gettext("Delete Membership Fee Type")}
>
<.icon name="hero-trash" class="size-4" />
</.button>
</:action>
</.table>
</div>
</div>
<%!-- About membership fee types (info above examples) --%>
<div class="mt-6 rounded-lg bg-base-200 p-4 prose prose-sm max-w-none">
<p>
{gettext(
"Membership fee types define different membership fee structures. Each type has a fixed interval (monthly, quarterly, half-yearly, yearly) that cannot be changed after creation."
)}
</p>
<ul>
<li>
<strong>{gettext("Name & Amount")}</strong>
- {gettext("Can be changed at any time. Amount changes affect future periods only.")}
</li>
<li>
<strong>{gettext("Interval")}</strong>
- {gettext(
"Fixed after creation. Members can only switch between types with the same interval."
)}
</li>
<li>
<strong>{gettext("Deletion")}</strong>
- {gettext("Only possible if no members are assigned to this type.")}
</li>
</ul>
</div>
<%!-- Examples (collapsible) --%>
<div class="mt-6 card bg-base-200">
<div class="card-body">
<details class="group">
<summary class="card-title cursor-pointer list-none flex items-center gap-2">
@ -303,117 +395,6 @@ defmodule MvWeb.MembershipFeeSettingsLive do
</details>
</div>
</div>
</div>
<%!-- Fee Types Table --%>
<div class="mt-8">
<h2 class="text-lg font-semibold mb-4">{gettext("Membership Fee Types")}</h2>
<.table
id="membership_fee_types"
rows={@membership_fee_types}
row_id={fn mft -> "mft-#{mft.id}" end}
>
<:col :let={mft} label={gettext("Name")}>
<span class="font-medium">{mft.name}</span>
<p :if={mft.description} class="text-sm text-base-content/70">{mft.description}</p>
</:col>
<:col :let={mft} label={gettext("Amount")}>
<span class="font-mono">{MembershipFeeHelpers.format_currency(mft.amount)}</span>
</:col>
<:col :let={mft} label={gettext("Interval")}>
<.badge variant="neutral" style="outline">
{MembershipFeeHelpers.format_interval(mft.interval)}
</.badge>
</:col>
<:col :let={mft} label={gettext("Members")}>
<span class="text-sm">{get_member_count(mft, @member_counts)}</span>
</:col>
<:action :let={mft}>
<.tooltip content={gettext("Edit membership fee type")} position="left">
<.button
variant="ghost"
size="sm"
navigate={~p"/membership_fee_settings/#{mft.id}/edit_fee_type"}
aria-label={gettext("Edit membership fee type")}
>
<.icon name="hero-pencil" class="size-4" />
</.button>
</.tooltip>
</:action>
<:action :let={mft}>
<.tooltip
:if={get_member_count(mft, @member_counts) > 0}
content={
gettext("Cannot delete - %{count} member(s) assigned",
count: get_member_count(mft, @member_counts)
)
}
position="left"
>
<button
phx-click="delete"
phx-value-id={mft.id}
data-confirm={gettext("Are you sure?")}
class="btn btn-ghost btn-xs text-error opacity-50 cursor-not-allowed"
aria-label={
gettext("Cannot delete - %{count} member(s) assigned",
count: get_member_count(mft, @member_counts)
)
}
disabled={true}
>
<.icon name="hero-trash" class="size-4" />
</button>
</.tooltip>
<.button
:if={get_member_count(mft, @member_counts) == 0}
variant="danger"
size="sm"
phx-click="delete"
phx-value-id={mft.id}
data-confirm={gettext("Are you sure?")}
aria-label={gettext("Delete Membership Fee Type")}
>
<.icon name="hero-trash" class="size-4" />
</.button>
</:action>
</.table>
<details class="mt-6 card bg-base-200">
<summary class="card-body cursor-pointer list-none card-title">
<.icon name="hero-information-circle" class="size-5" />
{gettext("About Membership Fee Types")}
</summary>
<div class="card-body pt-0 prose prose-sm max-w-none">
<p>
{gettext(
"Membership fee types define different membership fee structures. Each type has a fixed interval (monthly, quarterly, half-yearly, yearly) that cannot be changed after creation."
)}
</p>
<ul>
<li>
<strong>{gettext("Name & Amount")}</strong>
- {gettext("Can be changed at any time. Amount changes affect future periods only.")}
</li>
<li>
<strong>{gettext("Interval")}</strong>
- {gettext(
"Fixed after creation. Members can only switch between types with the same interval."
)}
</li>
<li>
<strong>{gettext("Deletion")}</strong>
- {gettext("Only possible if no members are assigned to this type.")}
</li>
</ul>
</div>
</details>
</div>
</Layouts.app>
"""
end

View file

@ -93,14 +93,14 @@ defmodule MvWeb.MembershipFeeTypeLive.IndexTest do
assert to == "/membership_fee_settings/new_fee_type"
end
test "edit button per row navigates to edit form", %{conn: conn, current_user: admin_user} do
test "row click navigates to edit form", %{conn: conn, current_user: admin_user} do
fee_type = create_fee_type(%{interval: :yearly}, admin_user)
{:ok, view, _html} = live(conn, "/membership_fee_settings")
{:error, {:live_redirect, %{to: to}}} =
view
|> element("a[href='/membership_fee_settings/#{fee_type.id}/edit_fee_type']")
|> element("#membership_fee_types tr#mft-#{fee_type.id} td:first-of-type")
|> render_click()
assert to == "/membership_fee_settings/#{fee_type.id}/edit_fee_type"