Membership fee settings: row-click table, compact default layout
This commit is contained in:
parent
60d3fa74fb
commit
f9d6936274
2 changed files with 180 additions and 199 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue