Merge branch 'main' into feature/278_membership_fee_settings
All checks were successful
continuous-integration/drone/push Build is passing
All checks were successful
continuous-integration/drone/push Build is passing
This commit is contained in:
commit
651f518215
19 changed files with 278 additions and 156 deletions
|
|
@ -77,7 +77,7 @@ defmodule MvWeb.Components.PaymentFilterComponent do
|
|||
phx-target={@myself}
|
||||
>
|
||||
<.icon name="hero-users" class="h-4 w-4" />
|
||||
{gettext("All")}
|
||||
{gettext("All payment statuses")}
|
||||
</button>
|
||||
</li>
|
||||
<li role="none">
|
||||
|
|
@ -140,7 +140,7 @@ defmodule MvWeb.Components.PaymentFilterComponent do
|
|||
defp parse_filter(_), do: nil
|
||||
|
||||
# Get display label for current filter
|
||||
defp filter_label(nil), do: gettext("All")
|
||||
defp filter_label(nil), do: gettext("All payment statuses")
|
||||
defp filter_label(:paid), do: gettext("Paid")
|
||||
defp filter_label(:not_paid), do: gettext("Not paid")
|
||||
end
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ defmodule MvWeb.CustomFieldLive.FormComponent do
|
|||
- Create new custom field definitions
|
||||
- Edit existing custom fields
|
||||
- Select value type from supported types
|
||||
- Set immutable and required flags
|
||||
- Set required flag
|
||||
- Real-time validation
|
||||
|
||||
## Props
|
||||
|
|
@ -50,10 +50,10 @@ defmodule MvWeb.CustomFieldLive.FormComponent do
|
|||
label={gettext("Value type")}
|
||||
options={
|
||||
Ash.Resource.Info.attribute(Mv.Membership.CustomField, :value_type).constraints[:one_of]
|
||||
|> Enum.map(fn type -> {MvWeb.Translations.FieldTypes.label(type), type} end)
|
||||
}
|
||||
/>
|
||||
<.input field={@form[:description]} type="text" label={gettext("Description")} />
|
||||
<.input field={@form[:immutable]} type="checkbox" label={gettext("Immutable")} />
|
||||
<.input field={@form[:required]} type="checkbox" label={gettext("Required")} />
|
||||
<.input
|
||||
field={@form[:show_in_overview]}
|
||||
|
|
@ -66,7 +66,7 @@ defmodule MvWeb.CustomFieldLive.FormComponent do
|
|||
{gettext("Cancel")}
|
||||
</.button>
|
||||
<.button phx-disable-with={gettext("Saving...")} variant="primary">
|
||||
{gettext("Save Custom field")}
|
||||
{gettext("Save Custom Field")}
|
||||
</.button>
|
||||
</div>
|
||||
</.form>
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ defmodule MvWeb.CustomFieldLive.IndexComponent do
|
|||
## Features
|
||||
- List all custom fields
|
||||
- Display type information (name, value type, description)
|
||||
- Show immutable and required flags
|
||||
- Show required flag
|
||||
- Create new custom fields
|
||||
- Edit existing custom fields
|
||||
- Delete custom fields with confirmation (cascades to all custom field values)
|
||||
|
|
@ -30,7 +30,7 @@ defmodule MvWeb.CustomFieldLive.IndexComponent do
|
|||
phx-click="new_custom_field"
|
||||
phx-target={@myself}
|
||||
>
|
||||
<.icon name="hero-plus" /> {gettext("New Custom field")}
|
||||
<.icon name="hero-plus" /> {gettext("New Custom Field")}
|
||||
</.button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -72,7 +72,7 @@ defmodule MvWeb.CustomFieldValueLive.Form do
|
|||
<% end %>
|
||||
|
||||
<.button phx-disable-with={gettext("Saving...")} variant="primary">
|
||||
{gettext("Save Custom field value")}
|
||||
{gettext("Save Custom Field Value")}
|
||||
</.button>
|
||||
<.button navigate={return_path(@return_to, @custom_field_value)}>{gettext("Cancel")}</.button>
|
||||
</.form>
|
||||
|
|
|
|||
|
|
@ -37,7 +37,7 @@ defmodule MvWeb.GlobalSettingsLive do
|
|||
@impl true
|
||||
def render(assigns) do
|
||||
~H"""
|
||||
<Layouts.app flash={@flash} current_user={@current_user}>
|
||||
<Layouts.app flash={@flash} current_user={@current_user} club_name={@settings.club_name}>
|
||||
<.header>
|
||||
{gettext("Settings")}
|
||||
<:subtitle>
|
||||
|
|
@ -80,10 +80,13 @@ defmodule MvWeb.GlobalSettingsLive do
|
|||
@impl true
|
||||
def handle_event("save", %{"setting" => setting_params}, socket) do
|
||||
case AshPhoenix.Form.submit(socket.assigns.form, params: setting_params) do
|
||||
{:ok, updated_settings} ->
|
||||
{:ok, _updated_settings} ->
|
||||
# Reload settings from database to ensure all dependent data is updated
|
||||
{:ok, fresh_settings} = Membership.get_settings()
|
||||
|
||||
socket =
|
||||
socket
|
||||
|> assign(:settings, updated_settings)
|
||||
|> assign(:settings, fresh_settings)
|
||||
|> put_flash(:info, gettext("Settings updated successfully"))
|
||||
|> assign_form()
|
||||
|
||||
|
|
|
|||
|
|
@ -145,7 +145,10 @@ defmodule MvWeb.MemberLive.Index do
|
|||
MapSet.put(socket.assigns.selected_members, id)
|
||||
end
|
||||
|
||||
{:noreply, assign(socket, :selected_members, selected)}
|
||||
{:noreply,
|
||||
socket
|
||||
|> assign(:selected_members, selected)
|
||||
|> update_selection_assigns()}
|
||||
end
|
||||
|
||||
@impl true
|
||||
|
|
@ -159,7 +162,10 @@ defmodule MvWeb.MemberLive.Index do
|
|||
all_ids
|
||||
end
|
||||
|
||||
{:noreply, assign(socket, :selected_members, selected)}
|
||||
{:noreply,
|
||||
socket
|
||||
|> assign(:selected_members, selected)
|
||||
|> update_selection_assigns()}
|
||||
end
|
||||
|
||||
@impl true
|
||||
|
|
@ -238,6 +244,7 @@ defmodule MvWeb.MemberLive.Index do
|
|||
socket
|
||||
|> assign(:query, q)
|
||||
|> load_members()
|
||||
|> update_selection_assigns()
|
||||
|
||||
existing_field_query = socket.assigns.sort_field
|
||||
existing_sort_query = socket.assigns.sort_order
|
||||
|
|
@ -263,6 +270,7 @@ defmodule MvWeb.MemberLive.Index do
|
|||
socket
|
||||
|> assign(:paid_filter, filter)
|
||||
|> load_members()
|
||||
|> update_selection_assigns()
|
||||
|
||||
# Build the URL with all params including new filter
|
||||
query_params =
|
||||
|
|
@ -309,6 +317,7 @@ defmodule MvWeb.MemberLive.Index do
|
|||
|> assign(:visible_custom_field_ids, extract_custom_field_ids(visible_custom_fields))
|
||||
|> load_members()
|
||||
|> prepare_dynamic_cols()
|
||||
|> update_selection_assigns()
|
||||
|> push_field_selection_url()
|
||||
|
||||
{:noreply, socket}
|
||||
|
|
@ -338,6 +347,7 @@ defmodule MvWeb.MemberLive.Index do
|
|||
|> assign(:visible_custom_field_ids, extract_custom_field_ids(visible_custom_fields))
|
||||
|> load_members()
|
||||
|> prepare_dynamic_cols()
|
||||
|> update_selection_assigns()
|
||||
|> push_field_selection_url()
|
||||
|
||||
{:noreply, socket}
|
||||
|
|
@ -389,6 +399,7 @@ defmodule MvWeb.MemberLive.Index do
|
|||
|> assign(:visible_custom_field_ids, extract_custom_field_ids(visible_custom_fields))
|
||||
|> load_members()
|
||||
|> prepare_dynamic_cols()
|
||||
|> update_selection_assigns()
|
||||
|
||||
{:noreply, socket}
|
||||
end
|
||||
|
|
@ -1112,4 +1123,34 @@ defmodule MvWeb.MemberLive.Index do
|
|||
|
||||
# Public helper function to format dates for use in templates
|
||||
def format_date(date), do: DateFormatter.format_date(date)
|
||||
|
||||
# Updates selection-related assigns (selected_count, any_selected?, mailto_bcc)
|
||||
# to avoid recalculating Enum.any? and Enum.count multiple times in templates.
|
||||
#
|
||||
# Note: Mailto URLs have length limits that vary by email client.
|
||||
# For large selections, consider using export functionality instead.
|
||||
defp update_selection_assigns(socket) do
|
||||
members = socket.assigns.members
|
||||
selected_members = socket.assigns.selected_members
|
||||
|
||||
selected_count =
|
||||
Enum.count(members, &MapSet.member?(selected_members, &1.id))
|
||||
|
||||
any_selected? =
|
||||
Enum.any?(members, &MapSet.member?(selected_members, &1.id))
|
||||
|
||||
mailto_bcc =
|
||||
if any_selected? do
|
||||
format_selected_member_emails(members, selected_members)
|
||||
|> Enum.join(", ")
|
||||
|> URI.encode_www_form()
|
||||
else
|
||||
""
|
||||
end
|
||||
|
||||
socket
|
||||
|> assign(:selected_count, selected_count)
|
||||
|> assign(:any_selected?, any_selected?)
|
||||
|> assign(:mailto_bcc, mailto_bcc)
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -3,23 +3,21 @@
|
|||
{gettext("Members")}
|
||||
<:actions>
|
||||
<.button
|
||||
:if={Enum.any?(@members, &MapSet.member?(@selected_members, &1.id))}
|
||||
class="secondary"
|
||||
id="copy-emails-btn"
|
||||
phx-hook="CopyToClipboard"
|
||||
phx-click="copy_emails"
|
||||
disabled={not @any_selected?}
|
||||
aria-label={gettext("Copy email addresses of selected members")}
|
||||
>
|
||||
<.icon name="hero-clipboard-document" />
|
||||
{gettext("Copy emails")} ({Enum.count(@members, &MapSet.member?(@selected_members, &1.id))})
|
||||
{gettext("Copy email addresses")} ({@selected_count})
|
||||
</.button>
|
||||
<.button
|
||||
:if={Enum.any?(@members, &MapSet.member?(@selected_members, &1.id))}
|
||||
href={
|
||||
"mailto:?bcc=" <>
|
||||
(MvWeb.MemberLive.Index.format_selected_member_emails(@members, @selected_members)
|
||||
|> Enum.join(", ")
|
||||
|> URI.encode())
|
||||
}
|
||||
class="secondary"
|
||||
id="open-email-btn"
|
||||
href={"mailto:?bcc=" <> @mailto_bcc}
|
||||
disabled={not @any_selected?}
|
||||
aria-label={gettext("Open email program with BCC recipients")}
|
||||
>
|
||||
<.icon name="hero-envelope" />
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue