feat: add payment status filter and paid column to member list
Add PaymentFilterComponent dropdown and colored paid column. Filter supports URL bookmarking and combines with search/sort.
This commit is contained in:
parent
88c5f3dde0
commit
671e6ce804
9 changed files with 814 additions and 78 deletions
140
lib/mv_web/live/components/payment_filter_component.ex
Normal file
140
lib/mv_web/live/components/payment_filter_component.ex
Normal file
|
|
@ -0,0 +1,140 @@
|
||||||
|
defmodule MvWeb.Components.PaymentFilterComponent do
|
||||||
|
@moduledoc """
|
||||||
|
Provides the PaymentFilter Live-Component.
|
||||||
|
|
||||||
|
A dropdown filter for filtering members by payment status (paid/not paid/all).
|
||||||
|
Uses DaisyUI dropdown styling and sends filter changes to parent LiveView.
|
||||||
|
|
||||||
|
## Props
|
||||||
|
- `:paid_filter` - Current filter state: `nil` (all), `:paid`, or `:not_paid`
|
||||||
|
- `:id` - Component ID (required)
|
||||||
|
- `:member_count` - Number of filtered members to display in badge (optional, default: 0)
|
||||||
|
|
||||||
|
## Events
|
||||||
|
- Sends `{:payment_filter_changed, filter}` to parent when filter changes
|
||||||
|
"""
|
||||||
|
use MvWeb, :live_component
|
||||||
|
|
||||||
|
@impl true
|
||||||
|
def mount(socket) do
|
||||||
|
{:ok, assign(socket, :open, false)}
|
||||||
|
end
|
||||||
|
|
||||||
|
@impl true
|
||||||
|
def update(assigns, socket) do
|
||||||
|
socket =
|
||||||
|
socket
|
||||||
|
|> assign(:id, assigns.id)
|
||||||
|
|> assign(:paid_filter, assigns[:paid_filter])
|
||||||
|
|> assign(:member_count, assigns[:member_count] || 0)
|
||||||
|
|
||||||
|
{:ok, socket}
|
||||||
|
end
|
||||||
|
|
||||||
|
@impl true
|
||||||
|
def render(assigns) do
|
||||||
|
~H"""
|
||||||
|
<div class="relative" id={@id}>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class={[
|
||||||
|
"btn btn-ghost gap-2",
|
||||||
|
@paid_filter && "btn-active"
|
||||||
|
]}
|
||||||
|
phx-click="toggle_dropdown"
|
||||||
|
phx-target={@myself}
|
||||||
|
aria-haspopup="true"
|
||||||
|
aria-expanded={to_string(@open)}
|
||||||
|
aria-label={gettext("Filter by payment status")}
|
||||||
|
>
|
||||||
|
<.icon name="hero-funnel" class="h-5 w-5" />
|
||||||
|
<span class="hidden sm:inline">{filter_label(@paid_filter)}</span>
|
||||||
|
<span :if={@paid_filter} class="badge badge-primary badge-sm">{@member_count}</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<ul
|
||||||
|
:if={@open}
|
||||||
|
class="menu dropdown-content bg-base-100 rounded-box z-10 w-52 p-2 shadow-lg absolute right-0 mt-2"
|
||||||
|
role="menu"
|
||||||
|
aria-label={gettext("Payment filter")}
|
||||||
|
phx-click-away="close_dropdown"
|
||||||
|
phx-target={@myself}
|
||||||
|
>
|
||||||
|
<li>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
role="menuitemradio"
|
||||||
|
aria-checked={to_string(@paid_filter == nil)}
|
||||||
|
class={@paid_filter == nil && "active"}
|
||||||
|
phx-click="select_filter"
|
||||||
|
phx-value-filter=""
|
||||||
|
phx-target={@myself}
|
||||||
|
>
|
||||||
|
<.icon name="hero-users" class="h-4 w-4" />
|
||||||
|
{gettext("All")}
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
role="menuitemradio"
|
||||||
|
aria-checked={to_string(@paid_filter == :paid)}
|
||||||
|
class={@paid_filter == :paid && "active"}
|
||||||
|
phx-click="select_filter"
|
||||||
|
phx-value-filter="paid"
|
||||||
|
phx-target={@myself}
|
||||||
|
>
|
||||||
|
<.icon name="hero-check-circle" class="h-4 w-4 text-success" />
|
||||||
|
{gettext("Paid")}
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
role="menuitemradio"
|
||||||
|
aria-checked={to_string(@paid_filter == :not_paid)}
|
||||||
|
class={@paid_filter == :not_paid && "active"}
|
||||||
|
phx-click="select_filter"
|
||||||
|
phx-value-filter="not_paid"
|
||||||
|
phx-target={@myself}
|
||||||
|
>
|
||||||
|
<.icon name="hero-x-circle" class="h-4 w-4 text-error" />
|
||||||
|
{gettext("Not paid")}
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
"""
|
||||||
|
end
|
||||||
|
|
||||||
|
@impl true
|
||||||
|
def handle_event("toggle_dropdown", _params, socket) do
|
||||||
|
{:noreply, assign(socket, :open, !socket.assigns.open)}
|
||||||
|
end
|
||||||
|
|
||||||
|
@impl true
|
||||||
|
def handle_event("close_dropdown", _params, socket) do
|
||||||
|
{:noreply, assign(socket, :open, false)}
|
||||||
|
end
|
||||||
|
|
||||||
|
@impl true
|
||||||
|
def handle_event("select_filter", %{"filter" => filter_str}, socket) do
|
||||||
|
filter = parse_filter(filter_str)
|
||||||
|
|
||||||
|
# Close dropdown and notify parent
|
||||||
|
socket = assign(socket, :open, false)
|
||||||
|
send(self(), {:payment_filter_changed, filter})
|
||||||
|
|
||||||
|
{:noreply, socket}
|
||||||
|
end
|
||||||
|
|
||||||
|
# Parse filter string to atom
|
||||||
|
defp parse_filter("paid"), do: :paid
|
||||||
|
defp parse_filter("not_paid"), do: :not_paid
|
||||||
|
defp parse_filter(_), do: nil
|
||||||
|
|
||||||
|
# Get display label for current filter
|
||||||
|
defp filter_label(nil), do: gettext("All")
|
||||||
|
defp filter_label(:paid), do: gettext("Paid")
|
||||||
|
defp filter_label(:not_paid), do: gettext("Not paid")
|
||||||
|
end
|
||||||
|
|
@ -46,7 +46,7 @@ defmodule MvWeb.MemberLive.Index do
|
||||||
Initializes the LiveView state.
|
Initializes the LiveView state.
|
||||||
|
|
||||||
Sets up initial assigns for page title, search query, sort configuration,
|
Sets up initial assigns for page title, search query, sort configuration,
|
||||||
and member selection. Actual data loading happens in `handle_params/3`.
|
payment filter, and member selection. Actual data loading happens in `handle_params/3`.
|
||||||
"""
|
"""
|
||||||
@impl true
|
@impl true
|
||||||
def mount(_params, _session, socket) do
|
def mount(_params, _session, socket) do
|
||||||
|
|
@ -74,6 +74,7 @@ defmodule MvWeb.MemberLive.Index do
|
||||||
|> assign(:query, "")
|
|> assign(:query, "")
|
||||||
|> assign_new(:sort_field, fn -> :first_name end)
|
|> assign_new(:sort_field, fn -> :first_name end)
|
||||||
|> assign_new(:sort_order, fn -> :asc end)
|
|> assign_new(:sort_order, fn -> :asc end)
|
||||||
|
|> assign(:paid_filter, nil)
|
||||||
|> assign(:selected_members, MapSet.new())
|
|> assign(:selected_members, MapSet.new())
|
||||||
|> assign(:custom_fields_visible, custom_fields_visible)
|
|> assign(:custom_fields_visible, custom_fields_visible)
|
||||||
|> assign(:member_fields_visible, get_visible_member_fields(settings))
|
|> assign(:member_fields_visible, get_visible_member_fields(settings))
|
||||||
|
|
@ -213,11 +214,8 @@ defmodule MvWeb.MemberLive.Index do
|
||||||
existing_sort_query = socket.assigns.sort_order
|
existing_sort_query = socket.assigns.sort_order
|
||||||
|
|
||||||
# Build the URL with queries
|
# Build the URL with queries
|
||||||
query_params = %{
|
query_params =
|
||||||
"query" => q,
|
build_query_params(q, existing_field_query, existing_sort_query, socket.assigns.paid_filter)
|
||||||
"sort_field" => existing_field_query,
|
|
||||||
"sort_order" => existing_sort_query
|
|
||||||
}
|
|
||||||
|
|
||||||
# Set the new path with params
|
# Set the new path with params
|
||||||
new_path = ~p"/members?#{query_params}"
|
new_path = ~p"/members?#{query_params}"
|
||||||
|
|
@ -230,13 +228,38 @@ defmodule MvWeb.MemberLive.Index do
|
||||||
)}
|
)}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@impl true
|
||||||
|
def handle_info({:payment_filter_changed, filter}, socket) do
|
||||||
|
socket =
|
||||||
|
socket
|
||||||
|
|> assign(:paid_filter, filter)
|
||||||
|
|> load_members(socket.assigns.query)
|
||||||
|
|
||||||
|
# Build the URL with all params including new filter
|
||||||
|
query_params =
|
||||||
|
build_query_params(
|
||||||
|
socket.assigns.query,
|
||||||
|
socket.assigns.sort_field,
|
||||||
|
socket.assigns.sort_order,
|
||||||
|
filter
|
||||||
|
)
|
||||||
|
|
||||||
|
new_path = ~p"/members?#{query_params}"
|
||||||
|
|
||||||
|
{:noreply,
|
||||||
|
push_patch(socket,
|
||||||
|
to: new_path,
|
||||||
|
replace: true
|
||||||
|
)}
|
||||||
|
end
|
||||||
|
|
||||||
# -----------------------------------------------------------------
|
# -----------------------------------------------------------------
|
||||||
# Handle Params from the URL
|
# Handle Params from the URL
|
||||||
# -----------------------------------------------------------------
|
# -----------------------------------------------------------------
|
||||||
@doc """
|
@doc """
|
||||||
Handles URL parameter changes.
|
Handles URL parameter changes.
|
||||||
|
|
||||||
Parses query parameters for search query, sort field, and sort order,
|
Parses query parameters for search query, sort field, sort order, and payment filter,
|
||||||
then loads members accordingly. This enables bookmarkable URLs and
|
then loads members accordingly. This enables bookmarkable URLs and
|
||||||
browser back/forward navigation.
|
browser back/forward navigation.
|
||||||
"""
|
"""
|
||||||
|
|
@ -246,6 +269,7 @@ defmodule MvWeb.MemberLive.Index do
|
||||||
socket
|
socket
|
||||||
|> maybe_update_search(params)
|
|> maybe_update_search(params)
|
||||||
|> maybe_update_sort(params)
|
|> maybe_update_sort(params)
|
||||||
|
|> maybe_update_paid_filter(params)
|
||||||
|> load_members(params["query"])
|
|> load_members(params["query"])
|
||||||
|> prepare_dynamic_cols()
|
|> prepare_dynamic_cols()
|
||||||
|
|
||||||
|
|
@ -337,11 +361,13 @@ defmodule MvWeb.MemberLive.Index do
|
||||||
field
|
field
|
||||||
end
|
end
|
||||||
|
|
||||||
query_params = %{
|
query_params =
|
||||||
"query" => socket.assigns.query,
|
build_query_params(
|
||||||
"sort_field" => field_str,
|
socket.assigns.query,
|
||||||
"sort_order" => Atom.to_string(order)
|
field_str,
|
||||||
}
|
Atom.to_string(order),
|
||||||
|
socket.assigns.paid_filter
|
||||||
|
)
|
||||||
|
|
||||||
new_path = ~p"/members?#{query_params}"
|
new_path = ~p"/members?#{query_params}"
|
||||||
|
|
||||||
|
|
@ -352,13 +378,45 @@ defmodule MvWeb.MemberLive.Index do
|
||||||
)}
|
)}
|
||||||
end
|
end
|
||||||
|
|
||||||
# Loads members from the database with custom field values and applies search/sort filters.
|
# Builds URL query parameters map including all filter/sort state.
|
||||||
|
# Converts paid_filter atom to string for URL.
|
||||||
|
defp build_query_params(query, sort_field, sort_order, paid_filter) do
|
||||||
|
field_str =
|
||||||
|
if is_atom(sort_field) do
|
||||||
|
Atom.to_string(sort_field)
|
||||||
|
else
|
||||||
|
sort_field
|
||||||
|
end
|
||||||
|
|
||||||
|
order_str =
|
||||||
|
if is_atom(sort_order) do
|
||||||
|
Atom.to_string(sort_order)
|
||||||
|
else
|
||||||
|
sort_order
|
||||||
|
end
|
||||||
|
|
||||||
|
base_params = %{
|
||||||
|
"query" => query,
|
||||||
|
"sort_field" => field_str,
|
||||||
|
"sort_order" => order_str
|
||||||
|
}
|
||||||
|
|
||||||
|
# Only add paid_filter to URL if it's set
|
||||||
|
case paid_filter do
|
||||||
|
nil -> base_params
|
||||||
|
:paid -> Map.put(base_params, "paid_filter", "paid")
|
||||||
|
:not_paid -> Map.put(base_params, "paid_filter", "not_paid")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Loads members from the database with custom field values and applies search/sort/payment filters.
|
||||||
#
|
#
|
||||||
# Process:
|
# Process:
|
||||||
# 1. Builds base query with selected fields
|
# 1. Builds base query with selected fields
|
||||||
# 2. Loads custom field values for visible custom fields (filtered at database level)
|
# 2. Loads custom field values for visible custom fields (filtered at database level)
|
||||||
# 3. Applies search filter if provided
|
# 3. Applies search filter if provided
|
||||||
# 4. Applies sorting (database-level for regular fields, in-memory for custom fields)
|
# 4. Applies payment status filter if set
|
||||||
|
# 5. Applies sorting (database-level for regular fields, in-memory for custom fields)
|
||||||
#
|
#
|
||||||
# Performance Considerations:
|
# Performance Considerations:
|
||||||
# - Database-level filtering: Custom field values are filtered directly in the database
|
# - Database-level filtering: Custom field values are filtered directly in the database
|
||||||
|
|
@ -383,6 +441,9 @@ defmodule MvWeb.MemberLive.Index do
|
||||||
# Apply the search filter first
|
# Apply the search filter first
|
||||||
query = apply_search_filter(query, search_query)
|
query = apply_search_filter(query, search_query)
|
||||||
|
|
||||||
|
# Apply payment status filter
|
||||||
|
query = apply_paid_filter(query, socket.assigns.paid_filter)
|
||||||
|
|
||||||
# Apply sorting based on current socket state
|
# Apply sorting based on current socket state
|
||||||
# For custom fields, we sort after loading
|
# For custom fields, we sort after loading
|
||||||
{query, sort_after_load} =
|
{query, sort_after_load} =
|
||||||
|
|
@ -457,6 +518,24 @@ defmodule MvWeb.MemberLive.Index do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Applies payment status filter to the query.
|
||||||
|
#
|
||||||
|
# Filter values:
|
||||||
|
# - nil: No filter, return all members
|
||||||
|
# - :paid: Only members with paid == true
|
||||||
|
# - :not_paid: Members with paid == false or paid == nil (not paid)
|
||||||
|
defp apply_paid_filter(query, nil), do: query
|
||||||
|
|
||||||
|
defp apply_paid_filter(query, :paid) do
|
||||||
|
Ash.Query.filter(query, expr(paid == true))
|
||||||
|
end
|
||||||
|
|
||||||
|
defp apply_paid_filter(query, :not_paid) do
|
||||||
|
# Include both false and nil as "not paid"
|
||||||
|
# Note: paid != true doesn't work correctly with NULL values in SQL
|
||||||
|
Ash.Query.filter(query, expr(paid == false or is_nil(paid)))
|
||||||
|
end
|
||||||
|
|
||||||
# Functions to toggle sorting order
|
# Functions to toggle sorting order
|
||||||
defp toggle_order(:asc), do: :desc
|
defp toggle_order(:asc), do: :desc
|
||||||
defp toggle_order(:desc), do: :asc
|
defp toggle_order(:desc), do: :asc
|
||||||
|
|
@ -747,6 +826,26 @@ defmodule MvWeb.MemberLive.Index do
|
||||||
socket
|
socket
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Updates paid filter from URL parameters if present.
|
||||||
|
#
|
||||||
|
# Validates the filter value, falling back to nil (no filter) if invalid.
|
||||||
|
defp maybe_update_paid_filter(socket, %{"paid_filter" => filter_str}) do
|
||||||
|
filter = determine_paid_filter(filter_str)
|
||||||
|
assign(socket, :paid_filter, filter)
|
||||||
|
end
|
||||||
|
|
||||||
|
defp maybe_update_paid_filter(socket, _params) do
|
||||||
|
# Reset filter if not in URL params
|
||||||
|
assign(socket, :paid_filter, nil)
|
||||||
|
end
|
||||||
|
|
||||||
|
# Determines valid paid filter from URL parameter.
|
||||||
|
#
|
||||||
|
# Only accepts "paid" or "not_paid", falls back to nil for invalid values.
|
||||||
|
defp determine_paid_filter("paid"), do: :paid
|
||||||
|
defp determine_paid_filter("not_paid"), do: :not_paid
|
||||||
|
defp determine_paid_filter(_), do: nil
|
||||||
|
|
||||||
# -------------------------------------------------------------
|
# -------------------------------------------------------------
|
||||||
# Helper Functions for Custom Field Values
|
# Helper Functions for Custom Field Values
|
||||||
# -------------------------------------------------------------
|
# -------------------------------------------------------------
|
||||||
|
|
|
||||||
|
|
@ -26,12 +26,20 @@
|
||||||
</:actions>
|
</:actions>
|
||||||
</.header>
|
</.header>
|
||||||
|
|
||||||
<.live_component
|
<div class="flex flex-wrap gap-4 items-center">
|
||||||
module={MvWeb.Components.SearchBarComponent}
|
<.live_component
|
||||||
id="search-bar"
|
module={MvWeb.Components.SearchBarComponent}
|
||||||
query={@query}
|
id="search-bar"
|
||||||
placeholder={gettext("Search...")}
|
query={@query}
|
||||||
/>
|
placeholder={gettext("Search...")}
|
||||||
|
/>
|
||||||
|
<.live_component
|
||||||
|
module={MvWeb.Components.PaymentFilterComponent}
|
||||||
|
id="payment-filter"
|
||||||
|
paid_filter={@paid_filter}
|
||||||
|
member_count={length(@members)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
<.table
|
<.table
|
||||||
id="members"
|
id="members"
|
||||||
|
|
@ -213,6 +221,14 @@
|
||||||
>
|
>
|
||||||
{member.join_date}
|
{member.join_date}
|
||||||
</:col>
|
</:col>
|
||||||
|
<:col :let={member} label={gettext("Paid")}>
|
||||||
|
<span class={[
|
||||||
|
"badge",
|
||||||
|
if(member.paid == true, do: "badge-success", else: "badge-error")
|
||||||
|
]}>
|
||||||
|
{if member.paid == true, do: gettext("Yes"), else: gettext("No")}
|
||||||
|
</span>
|
||||||
|
</:col>
|
||||||
<:action :let={member}>
|
<:action :let={member}>
|
||||||
<div class="sr-only">
|
<div class="sr-only">
|
||||||
<.link navigate={~p"/members/#{member}"}>{gettext("Show")}</.link>
|
<.link navigate={~p"/members/#{member}"}>{gettext("Show")}</.link>
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,7 @@ msgstr ""
|
||||||
msgid "Actions"
|
msgid "Actions"
|
||||||
msgstr "Aktionen"
|
msgstr "Aktionen"
|
||||||
|
|
||||||
#: lib/mv_web/live/member_live/index.html.heex:227
|
#: lib/mv_web/live/member_live/index.html.heex:243
|
||||||
#: lib/mv_web/live/user_live/index.html.heex:72
|
#: lib/mv_web/live/user_live/index.html.heex:72
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Are you sure?"
|
msgid "Are you sure?"
|
||||||
|
|
@ -28,19 +28,19 @@ msgid "Attempting to reconnect"
|
||||||
msgstr "Verbindung wird wiederhergestellt"
|
msgstr "Verbindung wird wiederhergestellt"
|
||||||
|
|
||||||
#: lib/mv_web/live/member_live/form.ex:54
|
#: lib/mv_web/live/member_live/form.ex:54
|
||||||
#: lib/mv_web/live/member_live/index.html.heex:171
|
#: lib/mv_web/live/member_live/index.html.heex:179
|
||||||
#: lib/mv_web/live/member_live/show.ex:59
|
#: lib/mv_web/live/member_live/show.ex:59
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "City"
|
msgid "City"
|
||||||
msgstr "Stadt"
|
msgstr "Stadt"
|
||||||
|
|
||||||
#: lib/mv_web/live/member_live/index.html.heex:229
|
#: lib/mv_web/live/member_live/index.html.heex:245
|
||||||
#: lib/mv_web/live/user_live/index.html.heex:74
|
#: lib/mv_web/live/user_live/index.html.heex:74
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Delete"
|
msgid "Delete"
|
||||||
msgstr "Löschen"
|
msgstr "Löschen"
|
||||||
|
|
||||||
#: lib/mv_web/live/member_live/index.html.heex:221
|
#: lib/mv_web/live/member_live/index.html.heex:237
|
||||||
#: lib/mv_web/live/user_live/form.ex:265
|
#: lib/mv_web/live/user_live/form.ex:265
|
||||||
#: lib/mv_web/live/user_live/index.html.heex:66
|
#: lib/mv_web/live/user_live/index.html.heex:66
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
|
|
@ -54,7 +54,7 @@ msgid "Edit Member"
|
||||||
msgstr "Mitglied bearbeiten"
|
msgstr "Mitglied bearbeiten"
|
||||||
|
|
||||||
#: lib/mv_web/live/member_live/form.ex:47
|
#: lib/mv_web/live/member_live/form.ex:47
|
||||||
#: lib/mv_web/live/member_live/index.html.heex:99
|
#: lib/mv_web/live/member_live/index.html.heex:107
|
||||||
#: lib/mv_web/live/member_live/show.ex:50
|
#: lib/mv_web/live/member_live/show.ex:50
|
||||||
#: lib/mv_web/live/user_live/form.ex:46
|
#: lib/mv_web/live/user_live/form.ex:46
|
||||||
#: lib/mv_web/live/user_live/index.html.heex:44
|
#: lib/mv_web/live/user_live/index.html.heex:44
|
||||||
|
|
@ -70,7 +70,7 @@ msgid "First Name"
|
||||||
msgstr "Vorname"
|
msgstr "Vorname"
|
||||||
|
|
||||||
#: lib/mv_web/live/member_live/form.ex:51
|
#: lib/mv_web/live/member_live/form.ex:51
|
||||||
#: lib/mv_web/live/member_live/index.html.heex:207
|
#: lib/mv_web/live/member_live/index.html.heex:215
|
||||||
#: lib/mv_web/live/member_live/show.ex:56
|
#: lib/mv_web/live/member_live/show.ex:56
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Join Date"
|
msgid "Join Date"
|
||||||
|
|
@ -87,7 +87,7 @@ msgstr "Nachname"
|
||||||
msgid "New Member"
|
msgid "New Member"
|
||||||
msgstr "Neues Mitglied"
|
msgstr "Neues Mitglied"
|
||||||
|
|
||||||
#: lib/mv_web/live/member_live/index.html.heex:218
|
#: lib/mv_web/live/member_live/index.html.heex:234
|
||||||
#: lib/mv_web/live/user_live/index.html.heex:63
|
#: lib/mv_web/live/user_live/index.html.heex:63
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Show"
|
msgid "Show"
|
||||||
|
|
@ -121,7 +121,7 @@ msgid "Exit Date"
|
||||||
msgstr "Austrittsdatum"
|
msgstr "Austrittsdatum"
|
||||||
|
|
||||||
#: lib/mv_web/live/member_live/form.ex:56
|
#: lib/mv_web/live/member_live/form.ex:56
|
||||||
#: lib/mv_web/live/member_live/index.html.heex:135
|
#: lib/mv_web/live/member_live/index.html.heex:143
|
||||||
#: lib/mv_web/live/member_live/show.ex:61
|
#: lib/mv_web/live/member_live/show.ex:61
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "House Number"
|
msgid "House Number"
|
||||||
|
|
@ -133,21 +133,24 @@ msgstr "Hausnummer"
|
||||||
msgid "Notes"
|
msgid "Notes"
|
||||||
msgstr "Notizen"
|
msgstr "Notizen"
|
||||||
|
|
||||||
|
#: lib/mv_web/live/components/payment_filter_component.ex:88
|
||||||
|
#: lib/mv_web/live/components/payment_filter_component.ex:138
|
||||||
#: lib/mv_web/live/member_live/form.ex:49
|
#: lib/mv_web/live/member_live/form.ex:49
|
||||||
|
#: lib/mv_web/live/member_live/index.html.heex:224
|
||||||
#: lib/mv_web/live/member_live/show.ex:52
|
#: lib/mv_web/live/member_live/show.ex:52
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Paid"
|
msgid "Paid"
|
||||||
msgstr "Bezahlt"
|
msgstr "Bezahlt"
|
||||||
|
|
||||||
#: lib/mv_web/live/member_live/form.ex:50
|
#: lib/mv_web/live/member_live/form.ex:50
|
||||||
#: lib/mv_web/live/member_live/index.html.heex:189
|
#: lib/mv_web/live/member_live/index.html.heex:197
|
||||||
#: lib/mv_web/live/member_live/show.ex:55
|
#: lib/mv_web/live/member_live/show.ex:55
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Phone Number"
|
msgid "Phone Number"
|
||||||
msgstr "Telefonnummer"
|
msgstr "Telefonnummer"
|
||||||
|
|
||||||
#: lib/mv_web/live/member_live/form.ex:57
|
#: lib/mv_web/live/member_live/form.ex:57
|
||||||
#: lib/mv_web/live/member_live/index.html.heex:153
|
#: lib/mv_web/live/member_live/index.html.heex:161
|
||||||
#: lib/mv_web/live/member_live/show.ex:62
|
#: lib/mv_web/live/member_live/show.ex:62
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Postal Code"
|
msgid "Postal Code"
|
||||||
|
|
@ -168,7 +171,7 @@ msgid "Saving..."
|
||||||
msgstr "Speichern..."
|
msgstr "Speichern..."
|
||||||
|
|
||||||
#: lib/mv_web/live/member_live/form.ex:55
|
#: lib/mv_web/live/member_live/form.ex:55
|
||||||
#: lib/mv_web/live/member_live/index.html.heex:117
|
#: lib/mv_web/live/member_live/index.html.heex:125
|
||||||
#: lib/mv_web/live/member_live/show.ex:60
|
#: lib/mv_web/live/member_live/show.ex:60
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Street"
|
msgid "Street"
|
||||||
|
|
@ -179,6 +182,7 @@ msgstr "Straße"
|
||||||
msgid "Id"
|
msgid "Id"
|
||||||
msgstr "ID"
|
msgstr "ID"
|
||||||
|
|
||||||
|
#: lib/mv_web/live/member_live/index.html.heex:229
|
||||||
#: lib/mv_web/live/member_live/index/formatter.ex:61
|
#: lib/mv_web/live/member_live/index/formatter.ex:61
|
||||||
#: lib/mv_web/live/member_live/show.ex:53
|
#: lib/mv_web/live/member_live/show.ex:53
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
|
|
@ -195,6 +199,7 @@ msgstr "Mitglied anzeigen"
|
||||||
msgid "This is a member record from your database."
|
msgid "This is a member record from your database."
|
||||||
msgstr "Dies ist ein Mitglied aus deiner Datenbank."
|
msgstr "Dies ist ein Mitglied aus deiner Datenbank."
|
||||||
|
|
||||||
|
#: lib/mv_web/live/member_live/index.html.heex:229
|
||||||
#: lib/mv_web/live/member_live/index/formatter.ex:60
|
#: lib/mv_web/live/member_live/index/formatter.ex:60
|
||||||
#: lib/mv_web/live/member_live/show.ex:53
|
#: lib/mv_web/live/member_live/show.ex:53
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
|
|
@ -360,12 +365,12 @@ msgstr "Profil"
|
||||||
msgid "Required"
|
msgid "Required"
|
||||||
msgstr "Erforderlich"
|
msgstr "Erforderlich"
|
||||||
|
|
||||||
#: lib/mv_web/live/member_live/index.html.heex:55
|
#: lib/mv_web/live/member_live/index.html.heex:63
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Select all members"
|
msgid "Select all members"
|
||||||
msgstr "Alle Mitglieder auswählen"
|
msgstr "Alle Mitglieder auswählen"
|
||||||
|
|
||||||
#: lib/mv_web/live/member_live/index.html.heex:69
|
#: lib/mv_web/live/member_live/index.html.heex:77
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Select member"
|
msgid "Select member"
|
||||||
msgstr "Mitglied auswählen"
|
msgstr "Mitglied auswählen"
|
||||||
|
|
@ -551,7 +556,7 @@ msgid "Toggle dark mode"
|
||||||
msgstr "Dunklen Modus umschalten"
|
msgstr "Dunklen Modus umschalten"
|
||||||
|
|
||||||
#: lib/mv_web/live/components/search_bar_component.ex:15
|
#: lib/mv_web/live/components/search_bar_component.ex:15
|
||||||
#: lib/mv_web/live/member_live/index.html.heex:33
|
#: lib/mv_web/live/member_live/index.html.heex:34
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Search..."
|
msgid "Search..."
|
||||||
msgstr "Suchen..."
|
msgstr "Suchen..."
|
||||||
|
|
@ -567,7 +572,7 @@ msgstr "Benutzer*innen"
|
||||||
msgid "Click to sort"
|
msgid "Click to sort"
|
||||||
msgstr "Klicke um zu sortieren"
|
msgstr "Klicke um zu sortieren"
|
||||||
|
|
||||||
#: lib/mv_web/live/member_live/index.html.heex:81
|
#: lib/mv_web/live/member_live/index.html.heex:89
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "First name"
|
msgid "First name"
|
||||||
msgstr "Vorname"
|
msgstr "Vorname"
|
||||||
|
|
@ -777,7 +782,7 @@ msgstr "Mitglied entverknüpfen"
|
||||||
msgid "Unlinking scheduled"
|
msgid "Unlinking scheduled"
|
||||||
msgstr "Entverknüpfung geplant"
|
msgstr "Entverknüpfung geplant"
|
||||||
|
|
||||||
#: lib/mv_web/live/member_live/index.ex:164
|
#: lib/mv_web/live/member_live/index.ex:165
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Copied %{count} email address to clipboard"
|
msgid "Copied %{count} email address to clipboard"
|
||||||
msgid_plural "Copied %{count} email addresses to clipboard"
|
msgid_plural "Copied %{count} email addresses to clipboard"
|
||||||
|
|
@ -794,12 +799,12 @@ msgstr "E-Mail-Adressen der ausgewählten Mitglieder kopieren"
|
||||||
msgid "Copy emails"
|
msgid "Copy emails"
|
||||||
msgstr "E-Mails kopieren"
|
msgstr "E-Mails kopieren"
|
||||||
|
|
||||||
#: lib/mv_web/live/member_live/index.ex:153
|
#: lib/mv_web/live/member_live/index.ex:154
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "No email addresses found"
|
msgid "No email addresses found"
|
||||||
msgstr "Keine E-Mail-Adressen gefunden"
|
msgstr "Keine E-Mail-Adressen gefunden"
|
||||||
|
|
||||||
#: lib/mv_web/live/member_live/index.ex:150
|
#: lib/mv_web/live/member_live/index.ex:151
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "No members selected"
|
msgid "No members selected"
|
||||||
msgstr "Keine Mitglieder ausgewählt"
|
msgstr "Keine Mitglieder ausgewählt"
|
||||||
|
|
@ -814,7 +819,7 @@ msgstr "E-Mail-Programm mit BCC-Empfänger*innen öffnen"
|
||||||
msgid "Open in email program"
|
msgid "Open in email program"
|
||||||
msgstr "Im E-Mail-Programm öffnen"
|
msgstr "Im E-Mail-Programm öffnen"
|
||||||
|
|
||||||
#: lib/mv_web/live/member_live/index.ex:173
|
#: lib/mv_web/live/member_live/index.ex:174
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Tip: Paste email addresses into the BCC field for privacy compliance"
|
msgid "Tip: Paste email addresses into the BCC field for privacy compliance"
|
||||||
msgstr "Tipp: E-Mail-Adressen ins BCC-Feld einfügen für Datenschutzkonformität"
|
msgstr "Tipp: E-Mail-Adressen ins BCC-Feld einfügen für Datenschutzkonformität"
|
||||||
|
|
@ -831,3 +836,25 @@ msgstr "Felder, die mit einem Sternchen (*) markiert sind, dürfen nicht leer bl
|
||||||
#, elixir-autogen, elixir-format, fuzzy
|
#, elixir-autogen, elixir-format, fuzzy
|
||||||
msgid "This field cannot be empty"
|
msgid "This field cannot be empty"
|
||||||
msgstr "Dieses Feld darf nicht leer bleiben"
|
msgstr "Dieses Feld darf nicht leer bleiben"
|
||||||
|
|
||||||
|
#: lib/mv_web/live/components/payment_filter_component.ex:74
|
||||||
|
#: lib/mv_web/live/components/payment_filter_component.ex:137
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "All"
|
||||||
|
msgstr "Alle"
|
||||||
|
|
||||||
|
#: lib/mv_web/live/components/payment_filter_component.ex:48
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "Filter by payment status"
|
||||||
|
msgstr "Nach Zahlungsstatus filtern"
|
||||||
|
|
||||||
|
#: lib/mv_web/live/components/payment_filter_component.ex:102
|
||||||
|
#: lib/mv_web/live/components/payment_filter_component.ex:139
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "Not paid"
|
||||||
|
msgstr "Nicht bezahlt"
|
||||||
|
|
||||||
|
#: lib/mv_web/live/components/payment_filter_component.ex:59
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "Payment filter"
|
||||||
|
msgstr "Zahlungsfilter"
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@ msgstr ""
|
||||||
msgid "Actions"
|
msgid "Actions"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/mv_web/live/member_live/index.html.heex:227
|
#: lib/mv_web/live/member_live/index.html.heex:243
|
||||||
#: lib/mv_web/live/user_live/index.html.heex:72
|
#: lib/mv_web/live/user_live/index.html.heex:72
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Are you sure?"
|
msgid "Are you sure?"
|
||||||
|
|
@ -29,19 +29,19 @@ msgid "Attempting to reconnect"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/mv_web/live/member_live/form.ex:54
|
#: lib/mv_web/live/member_live/form.ex:54
|
||||||
#: lib/mv_web/live/member_live/index.html.heex:171
|
#: lib/mv_web/live/member_live/index.html.heex:179
|
||||||
#: lib/mv_web/live/member_live/show.ex:59
|
#: lib/mv_web/live/member_live/show.ex:59
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "City"
|
msgid "City"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/mv_web/live/member_live/index.html.heex:229
|
#: lib/mv_web/live/member_live/index.html.heex:245
|
||||||
#: lib/mv_web/live/user_live/index.html.heex:74
|
#: lib/mv_web/live/user_live/index.html.heex:74
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Delete"
|
msgid "Delete"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/mv_web/live/member_live/index.html.heex:221
|
#: lib/mv_web/live/member_live/index.html.heex:237
|
||||||
#: lib/mv_web/live/user_live/form.ex:265
|
#: lib/mv_web/live/user_live/form.ex:265
|
||||||
#: lib/mv_web/live/user_live/index.html.heex:66
|
#: lib/mv_web/live/user_live/index.html.heex:66
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
|
|
@ -55,7 +55,7 @@ msgid "Edit Member"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/mv_web/live/member_live/form.ex:47
|
#: lib/mv_web/live/member_live/form.ex:47
|
||||||
#: lib/mv_web/live/member_live/index.html.heex:99
|
#: lib/mv_web/live/member_live/index.html.heex:107
|
||||||
#: lib/mv_web/live/member_live/show.ex:50
|
#: lib/mv_web/live/member_live/show.ex:50
|
||||||
#: lib/mv_web/live/user_live/form.ex:46
|
#: lib/mv_web/live/user_live/form.ex:46
|
||||||
#: lib/mv_web/live/user_live/index.html.heex:44
|
#: lib/mv_web/live/user_live/index.html.heex:44
|
||||||
|
|
@ -71,7 +71,7 @@ msgid "First Name"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/mv_web/live/member_live/form.ex:51
|
#: lib/mv_web/live/member_live/form.ex:51
|
||||||
#: lib/mv_web/live/member_live/index.html.heex:207
|
#: lib/mv_web/live/member_live/index.html.heex:215
|
||||||
#: lib/mv_web/live/member_live/show.ex:56
|
#: lib/mv_web/live/member_live/show.ex:56
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Join Date"
|
msgid "Join Date"
|
||||||
|
|
@ -88,7 +88,7 @@ msgstr ""
|
||||||
msgid "New Member"
|
msgid "New Member"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/mv_web/live/member_live/index.html.heex:218
|
#: lib/mv_web/live/member_live/index.html.heex:234
|
||||||
#: lib/mv_web/live/user_live/index.html.heex:63
|
#: lib/mv_web/live/user_live/index.html.heex:63
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Show"
|
msgid "Show"
|
||||||
|
|
@ -122,7 +122,7 @@ msgid "Exit Date"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/mv_web/live/member_live/form.ex:56
|
#: lib/mv_web/live/member_live/form.ex:56
|
||||||
#: lib/mv_web/live/member_live/index.html.heex:135
|
#: lib/mv_web/live/member_live/index.html.heex:143
|
||||||
#: lib/mv_web/live/member_live/show.ex:61
|
#: lib/mv_web/live/member_live/show.ex:61
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "House Number"
|
msgid "House Number"
|
||||||
|
|
@ -134,21 +134,24 @@ msgstr ""
|
||||||
msgid "Notes"
|
msgid "Notes"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/mv_web/live/components/payment_filter_component.ex:88
|
||||||
|
#: lib/mv_web/live/components/payment_filter_component.ex:138
|
||||||
#: lib/mv_web/live/member_live/form.ex:49
|
#: lib/mv_web/live/member_live/form.ex:49
|
||||||
|
#: lib/mv_web/live/member_live/index.html.heex:224
|
||||||
#: lib/mv_web/live/member_live/show.ex:52
|
#: lib/mv_web/live/member_live/show.ex:52
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Paid"
|
msgid "Paid"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/mv_web/live/member_live/form.ex:50
|
#: lib/mv_web/live/member_live/form.ex:50
|
||||||
#: lib/mv_web/live/member_live/index.html.heex:189
|
#: lib/mv_web/live/member_live/index.html.heex:197
|
||||||
#: lib/mv_web/live/member_live/show.ex:55
|
#: lib/mv_web/live/member_live/show.ex:55
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Phone Number"
|
msgid "Phone Number"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/mv_web/live/member_live/form.ex:57
|
#: lib/mv_web/live/member_live/form.ex:57
|
||||||
#: lib/mv_web/live/member_live/index.html.heex:153
|
#: lib/mv_web/live/member_live/index.html.heex:161
|
||||||
#: lib/mv_web/live/member_live/show.ex:62
|
#: lib/mv_web/live/member_live/show.ex:62
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Postal Code"
|
msgid "Postal Code"
|
||||||
|
|
@ -169,7 +172,7 @@ msgid "Saving..."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/mv_web/live/member_live/form.ex:55
|
#: lib/mv_web/live/member_live/form.ex:55
|
||||||
#: lib/mv_web/live/member_live/index.html.heex:117
|
#: lib/mv_web/live/member_live/index.html.heex:125
|
||||||
#: lib/mv_web/live/member_live/show.ex:60
|
#: lib/mv_web/live/member_live/show.ex:60
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Street"
|
msgid "Street"
|
||||||
|
|
@ -180,6 +183,7 @@ msgstr ""
|
||||||
msgid "Id"
|
msgid "Id"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/mv_web/live/member_live/index.html.heex:229
|
||||||
#: lib/mv_web/live/member_live/index/formatter.ex:61
|
#: lib/mv_web/live/member_live/index/formatter.ex:61
|
||||||
#: lib/mv_web/live/member_live/show.ex:53
|
#: lib/mv_web/live/member_live/show.ex:53
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
|
|
@ -196,6 +200,7 @@ msgstr ""
|
||||||
msgid "This is a member record from your database."
|
msgid "This is a member record from your database."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/mv_web/live/member_live/index.html.heex:229
|
||||||
#: lib/mv_web/live/member_live/index/formatter.ex:60
|
#: lib/mv_web/live/member_live/index/formatter.ex:60
|
||||||
#: lib/mv_web/live/member_live/show.ex:53
|
#: lib/mv_web/live/member_live/show.ex:53
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
|
|
@ -361,12 +366,12 @@ msgstr ""
|
||||||
msgid "Required"
|
msgid "Required"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/mv_web/live/member_live/index.html.heex:55
|
#: lib/mv_web/live/member_live/index.html.heex:63
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Select all members"
|
msgid "Select all members"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/mv_web/live/member_live/index.html.heex:69
|
#: lib/mv_web/live/member_live/index.html.heex:77
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Select member"
|
msgid "Select member"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
@ -552,7 +557,7 @@ msgid "Toggle dark mode"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/mv_web/live/components/search_bar_component.ex:15
|
#: lib/mv_web/live/components/search_bar_component.ex:15
|
||||||
#: lib/mv_web/live/member_live/index.html.heex:33
|
#: lib/mv_web/live/member_live/index.html.heex:34
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Search..."
|
msgid "Search..."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
@ -568,7 +573,7 @@ msgstr ""
|
||||||
msgid "Click to sort"
|
msgid "Click to sort"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/mv_web/live/member_live/index.html.heex:81
|
#: lib/mv_web/live/member_live/index.html.heex:89
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "First name"
|
msgid "First name"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
@ -778,7 +783,7 @@ msgstr ""
|
||||||
msgid "Unlinking scheduled"
|
msgid "Unlinking scheduled"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/mv_web/live/member_live/index.ex:164
|
#: lib/mv_web/live/member_live/index.ex:165
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Copied %{count} email address to clipboard"
|
msgid "Copied %{count} email address to clipboard"
|
||||||
msgid_plural "Copied %{count} email addresses to clipboard"
|
msgid_plural "Copied %{count} email addresses to clipboard"
|
||||||
|
|
@ -795,12 +800,12 @@ msgstr ""
|
||||||
msgid "Copy emails"
|
msgid "Copy emails"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/mv_web/live/member_live/index.ex:153
|
#: lib/mv_web/live/member_live/index.ex:154
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "No email addresses found"
|
msgid "No email addresses found"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/mv_web/live/member_live/index.ex:150
|
#: lib/mv_web/live/member_live/index.ex:151
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "No members selected"
|
msgid "No members selected"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
@ -815,7 +820,7 @@ msgstr ""
|
||||||
msgid "Open in email program"
|
msgid "Open in email program"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/mv_web/live/member_live/index.ex:173
|
#: lib/mv_web/live/member_live/index.ex:174
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Tip: Paste email addresses into the BCC field for privacy compliance"
|
msgid "Tip: Paste email addresses into the BCC field for privacy compliance"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
@ -832,3 +837,25 @@ msgstr ""
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "This field cannot be empty"
|
msgid "This field cannot be empty"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/mv_web/live/components/payment_filter_component.ex:74
|
||||||
|
#: lib/mv_web/live/components/payment_filter_component.ex:137
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "All"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/mv_web/live/components/payment_filter_component.ex:48
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "Filter by payment status"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/mv_web/live/components/payment_filter_component.ex:102
|
||||||
|
#: lib/mv_web/live/components/payment_filter_component.ex:139
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "Not paid"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/mv_web/live/components/payment_filter_component.ex:59
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "Payment filter"
|
||||||
|
msgstr ""
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@ msgstr ""
|
||||||
msgid "Actions"
|
msgid "Actions"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/mv_web/live/member_live/index.html.heex:227
|
#: lib/mv_web/live/member_live/index.html.heex:243
|
||||||
#: lib/mv_web/live/user_live/index.html.heex:72
|
#: lib/mv_web/live/user_live/index.html.heex:72
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Are you sure?"
|
msgid "Are you sure?"
|
||||||
|
|
@ -29,19 +29,19 @@ msgid "Attempting to reconnect"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/mv_web/live/member_live/form.ex:54
|
#: lib/mv_web/live/member_live/form.ex:54
|
||||||
#: lib/mv_web/live/member_live/index.html.heex:171
|
#: lib/mv_web/live/member_live/index.html.heex:179
|
||||||
#: lib/mv_web/live/member_live/show.ex:59
|
#: lib/mv_web/live/member_live/show.ex:59
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "City"
|
msgid "City"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/mv_web/live/member_live/index.html.heex:229
|
#: lib/mv_web/live/member_live/index.html.heex:245
|
||||||
#: lib/mv_web/live/user_live/index.html.heex:74
|
#: lib/mv_web/live/user_live/index.html.heex:74
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Delete"
|
msgid "Delete"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/mv_web/live/member_live/index.html.heex:221
|
#: lib/mv_web/live/member_live/index.html.heex:237
|
||||||
#: lib/mv_web/live/user_live/form.ex:265
|
#: lib/mv_web/live/user_live/form.ex:265
|
||||||
#: lib/mv_web/live/user_live/index.html.heex:66
|
#: lib/mv_web/live/user_live/index.html.heex:66
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
|
|
@ -55,7 +55,7 @@ msgid "Edit Member"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/mv_web/live/member_live/form.ex:47
|
#: lib/mv_web/live/member_live/form.ex:47
|
||||||
#: lib/mv_web/live/member_live/index.html.heex:99
|
#: lib/mv_web/live/member_live/index.html.heex:107
|
||||||
#: lib/mv_web/live/member_live/show.ex:50
|
#: lib/mv_web/live/member_live/show.ex:50
|
||||||
#: lib/mv_web/live/user_live/form.ex:46
|
#: lib/mv_web/live/user_live/form.ex:46
|
||||||
#: lib/mv_web/live/user_live/index.html.heex:44
|
#: lib/mv_web/live/user_live/index.html.heex:44
|
||||||
|
|
@ -71,7 +71,7 @@ msgid "First Name"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/mv_web/live/member_live/form.ex:51
|
#: lib/mv_web/live/member_live/form.ex:51
|
||||||
#: lib/mv_web/live/member_live/index.html.heex:207
|
#: lib/mv_web/live/member_live/index.html.heex:215
|
||||||
#: lib/mv_web/live/member_live/show.ex:56
|
#: lib/mv_web/live/member_live/show.ex:56
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Join Date"
|
msgid "Join Date"
|
||||||
|
|
@ -88,7 +88,7 @@ msgstr ""
|
||||||
msgid "New Member"
|
msgid "New Member"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/mv_web/live/member_live/index.html.heex:218
|
#: lib/mv_web/live/member_live/index.html.heex:234
|
||||||
#: lib/mv_web/live/user_live/index.html.heex:63
|
#: lib/mv_web/live/user_live/index.html.heex:63
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Show"
|
msgid "Show"
|
||||||
|
|
@ -122,7 +122,7 @@ msgid "Exit Date"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/mv_web/live/member_live/form.ex:56
|
#: lib/mv_web/live/member_live/form.ex:56
|
||||||
#: lib/mv_web/live/member_live/index.html.heex:135
|
#: lib/mv_web/live/member_live/index.html.heex:143
|
||||||
#: lib/mv_web/live/member_live/show.ex:61
|
#: lib/mv_web/live/member_live/show.ex:61
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "House Number"
|
msgid "House Number"
|
||||||
|
|
@ -134,21 +134,24 @@ msgstr ""
|
||||||
msgid "Notes"
|
msgid "Notes"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/mv_web/live/components/payment_filter_component.ex:88
|
||||||
|
#: lib/mv_web/live/components/payment_filter_component.ex:138
|
||||||
#: lib/mv_web/live/member_live/form.ex:49
|
#: lib/mv_web/live/member_live/form.ex:49
|
||||||
|
#: lib/mv_web/live/member_live/index.html.heex:224
|
||||||
#: lib/mv_web/live/member_live/show.ex:52
|
#: lib/mv_web/live/member_live/show.ex:52
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Paid"
|
msgid "Paid"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/mv_web/live/member_live/form.ex:50
|
#: lib/mv_web/live/member_live/form.ex:50
|
||||||
#: lib/mv_web/live/member_live/index.html.heex:189
|
#: lib/mv_web/live/member_live/index.html.heex:197
|
||||||
#: lib/mv_web/live/member_live/show.ex:55
|
#: lib/mv_web/live/member_live/show.ex:55
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Phone Number"
|
msgid "Phone Number"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/mv_web/live/member_live/form.ex:57
|
#: lib/mv_web/live/member_live/form.ex:57
|
||||||
#: lib/mv_web/live/member_live/index.html.heex:153
|
#: lib/mv_web/live/member_live/index.html.heex:161
|
||||||
#: lib/mv_web/live/member_live/show.ex:62
|
#: lib/mv_web/live/member_live/show.ex:62
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Postal Code"
|
msgid "Postal Code"
|
||||||
|
|
@ -169,7 +172,7 @@ msgid "Saving..."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/mv_web/live/member_live/form.ex:55
|
#: lib/mv_web/live/member_live/form.ex:55
|
||||||
#: lib/mv_web/live/member_live/index.html.heex:117
|
#: lib/mv_web/live/member_live/index.html.heex:125
|
||||||
#: lib/mv_web/live/member_live/show.ex:60
|
#: lib/mv_web/live/member_live/show.ex:60
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Street"
|
msgid "Street"
|
||||||
|
|
@ -180,6 +183,7 @@ msgstr ""
|
||||||
msgid "Id"
|
msgid "Id"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/mv_web/live/member_live/index.html.heex:229
|
||||||
#: lib/mv_web/live/member_live/index/formatter.ex:61
|
#: lib/mv_web/live/member_live/index/formatter.ex:61
|
||||||
#: lib/mv_web/live/member_live/show.ex:53
|
#: lib/mv_web/live/member_live/show.ex:53
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
|
|
@ -196,6 +200,7 @@ msgstr ""
|
||||||
msgid "This is a member record from your database."
|
msgid "This is a member record from your database."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/mv_web/live/member_live/index.html.heex:229
|
||||||
#: lib/mv_web/live/member_live/index/formatter.ex:60
|
#: lib/mv_web/live/member_live/index/formatter.ex:60
|
||||||
#: lib/mv_web/live/member_live/show.ex:53
|
#: lib/mv_web/live/member_live/show.ex:53
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
|
|
@ -361,12 +366,12 @@ msgstr ""
|
||||||
msgid "Required"
|
msgid "Required"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/mv_web/live/member_live/index.html.heex:55
|
#: lib/mv_web/live/member_live/index.html.heex:63
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Select all members"
|
msgid "Select all members"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/mv_web/live/member_live/index.html.heex:69
|
#: lib/mv_web/live/member_live/index.html.heex:77
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Select member"
|
msgid "Select member"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
@ -552,7 +557,7 @@ msgid "Toggle dark mode"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/mv_web/live/components/search_bar_component.ex:15
|
#: lib/mv_web/live/components/search_bar_component.ex:15
|
||||||
#: lib/mv_web/live/member_live/index.html.heex:33
|
#: lib/mv_web/live/member_live/index.html.heex:34
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Search..."
|
msgid "Search..."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
@ -568,7 +573,7 @@ msgstr ""
|
||||||
msgid "Click to sort"
|
msgid "Click to sort"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/mv_web/live/member_live/index.html.heex:81
|
#: lib/mv_web/live/member_live/index.html.heex:89
|
||||||
#, elixir-autogen, elixir-format, fuzzy
|
#, elixir-autogen, elixir-format, fuzzy
|
||||||
msgid "First name"
|
msgid "First name"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
@ -778,7 +783,7 @@ msgstr ""
|
||||||
msgid "Unlinking scheduled"
|
msgid "Unlinking scheduled"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/mv_web/live/member_live/index.ex:164
|
#: lib/mv_web/live/member_live/index.ex:165
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Copied %{count} email address to clipboard"
|
msgid "Copied %{count} email address to clipboard"
|
||||||
msgid_plural "Copied %{count} email addresses to clipboard"
|
msgid_plural "Copied %{count} email addresses to clipboard"
|
||||||
|
|
@ -795,12 +800,12 @@ msgstr ""
|
||||||
msgid "Copy emails"
|
msgid "Copy emails"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/mv_web/live/member_live/index.ex:153
|
#: lib/mv_web/live/member_live/index.ex:154
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "No email addresses found"
|
msgid "No email addresses found"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/mv_web/live/member_live/index.ex:150
|
#: lib/mv_web/live/member_live/index.ex:151
|
||||||
#, elixir-autogen, elixir-format, fuzzy
|
#, elixir-autogen, elixir-format, fuzzy
|
||||||
msgid "No members selected"
|
msgid "No members selected"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
@ -815,7 +820,7 @@ msgstr ""
|
||||||
msgid "Open in email program"
|
msgid "Open in email program"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/mv_web/live/member_live/index.ex:173
|
#: lib/mv_web/live/member_live/index.ex:174
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Tip: Paste email addresses into the BCC field for privacy compliance"
|
msgid "Tip: Paste email addresses into the BCC field for privacy compliance"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
@ -832,3 +837,25 @@ msgstr ""
|
||||||
#, elixir-autogen, elixir-format, fuzzy
|
#, elixir-autogen, elixir-format, fuzzy
|
||||||
msgid "This field cannot be empty"
|
msgid "This field cannot be empty"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/mv_web/live/components/payment_filter_component.ex:74
|
||||||
|
#: lib/mv_web/live/components/payment_filter_component.ex:137
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "All"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/mv_web/live/components/payment_filter_component.ex:48
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "Filter by payment status"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/mv_web/live/components/payment_filter_component.ex:102
|
||||||
|
#: lib/mv_web/live/components/payment_filter_component.ex:139
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "Not paid"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/mv_web/live/components/payment_filter_component.ex:59
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "Payment filter"
|
||||||
|
msgstr ""
|
||||||
|
|
|
||||||
182
test/mv_web/components/payment_filter_component_test.exs
Normal file
182
test/mv_web/components/payment_filter_component_test.exs
Normal file
|
|
@ -0,0 +1,182 @@
|
||||||
|
defmodule MvWeb.Components.PaymentFilterComponentTest do
|
||||||
|
@moduledoc """
|
||||||
|
Unit tests for the PaymentFilterComponent.
|
||||||
|
|
||||||
|
Tests cover:
|
||||||
|
- Rendering in all 3 filter states (nil, :paid, :not_paid)
|
||||||
|
- Event emission when selecting options
|
||||||
|
- ARIA attributes for accessibility
|
||||||
|
- Dropdown open/close behavior
|
||||||
|
"""
|
||||||
|
use MvWeb.ConnCase, async: true
|
||||||
|
|
||||||
|
import Phoenix.LiveViewTest
|
||||||
|
|
||||||
|
describe "rendering" do
|
||||||
|
test "renders with no filter active (nil)", %{conn: conn} do
|
||||||
|
conn = conn_with_oidc_user(conn)
|
||||||
|
{:ok, view, _html} = live(conn, "/members")
|
||||||
|
|
||||||
|
# Should show "All" text and no badge
|
||||||
|
assert has_element?(view, "#payment-filter")
|
||||||
|
refute has_element?(view, "#payment-filter .badge")
|
||||||
|
end
|
||||||
|
|
||||||
|
test "renders with paid filter active", %{conn: conn} do
|
||||||
|
conn = conn_with_oidc_user(conn)
|
||||||
|
{:ok, view, _html} = live(conn, "/members?paid_filter=paid")
|
||||||
|
|
||||||
|
# Should show badge when filter is active
|
||||||
|
assert has_element?(view, "#payment-filter .badge")
|
||||||
|
end
|
||||||
|
|
||||||
|
test "renders with not_paid filter active", %{conn: conn} do
|
||||||
|
conn = conn_with_oidc_user(conn)
|
||||||
|
{:ok, view, _html} = live(conn, "/members?paid_filter=not_paid")
|
||||||
|
|
||||||
|
# Should show badge when filter is active
|
||||||
|
assert has_element?(view, "#payment-filter .badge")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "dropdown behavior" do
|
||||||
|
test "dropdown opens on button click", %{conn: conn} do
|
||||||
|
conn = conn_with_oidc_user(conn)
|
||||||
|
{:ok, view, _html} = live(conn, "/members")
|
||||||
|
|
||||||
|
# Initially dropdown is closed
|
||||||
|
refute has_element?(view, "#payment-filter ul[role='menu']")
|
||||||
|
|
||||||
|
# Click to open
|
||||||
|
view
|
||||||
|
|> element("#payment-filter button[aria-haspopup='true']")
|
||||||
|
|> render_click()
|
||||||
|
|
||||||
|
# Dropdown should be visible
|
||||||
|
assert has_element?(view, "#payment-filter ul[role='menu']")
|
||||||
|
end
|
||||||
|
|
||||||
|
test "dropdown closes after selecting an option", %{conn: conn} do
|
||||||
|
conn = conn_with_oidc_user(conn)
|
||||||
|
{:ok, view, _html} = live(conn, "/members")
|
||||||
|
|
||||||
|
# Open dropdown
|
||||||
|
view
|
||||||
|
|> element("#payment-filter button[aria-haspopup='true']")
|
||||||
|
|> render_click()
|
||||||
|
|
||||||
|
assert has_element?(view, "#payment-filter ul[role='menu']")
|
||||||
|
|
||||||
|
# Select an option - this should close the dropdown
|
||||||
|
view
|
||||||
|
|> element("#payment-filter button[phx-value-filter='paid']")
|
||||||
|
|> render_click()
|
||||||
|
|
||||||
|
# After selection, dropdown should be closed
|
||||||
|
# Note: The dropdown closes via assign, which is reflected in the next render
|
||||||
|
refute has_element?(view, "#payment-filter ul[role='menu']")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "filter selection" do
|
||||||
|
test "selecting 'All' clears the filter and updates URL", %{conn: conn} do
|
||||||
|
conn = conn_with_oidc_user(conn)
|
||||||
|
{:ok, view, _html} = live(conn, "/members?paid_filter=paid")
|
||||||
|
|
||||||
|
# Open dropdown
|
||||||
|
view
|
||||||
|
|> element("#payment-filter button[aria-haspopup='true']")
|
||||||
|
|> render_click()
|
||||||
|
|
||||||
|
# Select "All" option
|
||||||
|
view
|
||||||
|
|> element("#payment-filter button[phx-value-filter='']")
|
||||||
|
|> render_click()
|
||||||
|
|
||||||
|
# URL should not contain paid_filter param - wait for patch
|
||||||
|
assert_patch(view)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "selecting 'Paid' sets the filter and updates URL", %{conn: conn} do
|
||||||
|
conn = conn_with_oidc_user(conn)
|
||||||
|
{:ok, view, _html} = live(conn, "/members")
|
||||||
|
|
||||||
|
# Open dropdown
|
||||||
|
view
|
||||||
|
|> element("#payment-filter button[aria-haspopup='true']")
|
||||||
|
|> render_click()
|
||||||
|
|
||||||
|
# Select "Paid" option
|
||||||
|
view
|
||||||
|
|> element("#payment-filter button[phx-value-filter='paid']")
|
||||||
|
|> render_click()
|
||||||
|
|
||||||
|
# Wait for patch and check URL contains paid_filter=paid
|
||||||
|
path = assert_patch(view)
|
||||||
|
assert path =~ "paid_filter=paid"
|
||||||
|
end
|
||||||
|
|
||||||
|
test "selecting 'Not paid' sets the filter and updates URL", %{conn: conn} do
|
||||||
|
conn = conn_with_oidc_user(conn)
|
||||||
|
{:ok, view, _html} = live(conn, "/members")
|
||||||
|
|
||||||
|
# Open dropdown
|
||||||
|
view
|
||||||
|
|> element("#payment-filter button[aria-haspopup='true']")
|
||||||
|
|> render_click()
|
||||||
|
|
||||||
|
# Select "Not paid" option
|
||||||
|
view
|
||||||
|
|> element("#payment-filter button[phx-value-filter='not_paid']")
|
||||||
|
|> render_click()
|
||||||
|
|
||||||
|
# Wait for patch and check URL contains paid_filter=not_paid
|
||||||
|
path = assert_patch(view)
|
||||||
|
assert path =~ "paid_filter=not_paid"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "accessibility" do
|
||||||
|
test "has correct ARIA attributes", %{conn: conn} do
|
||||||
|
conn = conn_with_oidc_user(conn)
|
||||||
|
{:ok, view, html} = live(conn, "/members")
|
||||||
|
|
||||||
|
# Main button should have aria-haspopup and aria-expanded
|
||||||
|
assert html =~ ~s(aria-haspopup="true")
|
||||||
|
assert html =~ ~s(aria-expanded="false")
|
||||||
|
assert html =~ ~s(aria-label=)
|
||||||
|
|
||||||
|
# Open dropdown
|
||||||
|
view
|
||||||
|
|> element("#payment-filter button[aria-haspopup='true']")
|
||||||
|
|> render_click()
|
||||||
|
|
||||||
|
html = render(view)
|
||||||
|
|
||||||
|
# Check aria-expanded is now true
|
||||||
|
assert html =~ ~s(aria-expanded="true")
|
||||||
|
|
||||||
|
# Menu should have role="menu"
|
||||||
|
assert html =~ ~s(role="menu")
|
||||||
|
|
||||||
|
# Options should have role="menuitemradio"
|
||||||
|
assert html =~ ~s(role="menuitemradio")
|
||||||
|
end
|
||||||
|
|
||||||
|
test "has aria-checked on selected option", %{conn: conn} do
|
||||||
|
conn = conn_with_oidc_user(conn)
|
||||||
|
{:ok, view, _html} = live(conn, "/members?paid_filter=paid")
|
||||||
|
|
||||||
|
# Open dropdown
|
||||||
|
view
|
||||||
|
|> element("#payment-filter button[aria-haspopup='true']")
|
||||||
|
|> render_click()
|
||||||
|
|
||||||
|
html = render(view)
|
||||||
|
|
||||||
|
# "Paid" option should have aria-checked="true"
|
||||||
|
# Check both possible orderings of attributes
|
||||||
|
assert html =~ "aria-checked=\"true\"" and html =~ "phx-value-filter=\"paid\""
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
@ -9,7 +9,8 @@ defmodule MvWeb.MemberLive.IndexCustomFieldsDisplayTest do
|
||||||
- Custom field values are correctly formatted for different types
|
- Custom field values are correctly formatted for different types
|
||||||
- Members without custom field values show empty cell or "-"
|
- Members without custom field values show empty cell or "-"
|
||||||
"""
|
"""
|
||||||
use MvWeb.ConnCase, async: true
|
# async: false to prevent PostgreSQL deadlocks when creating members and custom fields
|
||||||
|
use MvWeb.ConnCase, async: false
|
||||||
import Phoenix.LiveViewTest
|
import Phoenix.LiveViewTest
|
||||||
require Ash.Query
|
require Ash.Query
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -469,4 +469,221 @@ defmodule MvWeb.MemberLive.IndexTest do
|
||||||
assert has_element?(view, "#flash-group")
|
assert has_element?(view, "#flash-group")
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe "payment filter integration" do
|
||||||
|
setup do
|
||||||
|
# Create members with different payment status
|
||||||
|
# Use unique names that won't appear elsewhere in the HTML
|
||||||
|
{:ok, paid_member} =
|
||||||
|
Mv.Membership.create_member(%{
|
||||||
|
first_name: "Zahler",
|
||||||
|
last_name: "Mitglied",
|
||||||
|
email: "zahler@example.com",
|
||||||
|
paid: true
|
||||||
|
})
|
||||||
|
|
||||||
|
{:ok, unpaid_member} =
|
||||||
|
Mv.Membership.create_member(%{
|
||||||
|
first_name: "Nichtzahler",
|
||||||
|
last_name: "Mitglied",
|
||||||
|
email: "nichtzahler@example.com",
|
||||||
|
paid: false
|
||||||
|
})
|
||||||
|
|
||||||
|
{:ok, nil_paid_member} =
|
||||||
|
Mv.Membership.create_member(%{
|
||||||
|
first_name: "Unbestimmt",
|
||||||
|
last_name: "Mitglied",
|
||||||
|
email: "unbestimmt@example.com"
|
||||||
|
# paid is nil by default
|
||||||
|
})
|
||||||
|
|
||||||
|
%{paid_member: paid_member, unpaid_member: unpaid_member, nil_paid_member: nil_paid_member}
|
||||||
|
end
|
||||||
|
|
||||||
|
test "filter shows all members when no filter is active", %{
|
||||||
|
conn: conn,
|
||||||
|
paid_member: paid_member,
|
||||||
|
unpaid_member: unpaid_member,
|
||||||
|
nil_paid_member: nil_paid_member
|
||||||
|
} do
|
||||||
|
conn = conn_with_oidc_user(conn)
|
||||||
|
{:ok, _view, html} = live(conn, "/members")
|
||||||
|
|
||||||
|
assert html =~ paid_member.first_name
|
||||||
|
assert html =~ unpaid_member.first_name
|
||||||
|
assert html =~ nil_paid_member.first_name
|
||||||
|
end
|
||||||
|
|
||||||
|
test "filter shows only paid members when paid filter is active", %{
|
||||||
|
conn: conn,
|
||||||
|
paid_member: paid_member,
|
||||||
|
unpaid_member: unpaid_member,
|
||||||
|
nil_paid_member: nil_paid_member
|
||||||
|
} do
|
||||||
|
conn = conn_with_oidc_user(conn)
|
||||||
|
{:ok, _view, html} = live(conn, "/members?paid_filter=paid")
|
||||||
|
|
||||||
|
assert html =~ paid_member.first_name
|
||||||
|
refute html =~ unpaid_member.first_name
|
||||||
|
refute html =~ nil_paid_member.first_name
|
||||||
|
end
|
||||||
|
|
||||||
|
test "filter shows only unpaid members (including nil) when not_paid filter is active", %{
|
||||||
|
conn: conn,
|
||||||
|
paid_member: paid_member,
|
||||||
|
unpaid_member: unpaid_member,
|
||||||
|
nil_paid_member: nil_paid_member
|
||||||
|
} do
|
||||||
|
conn = conn_with_oidc_user(conn)
|
||||||
|
{:ok, _view, html} = live(conn, "/members?paid_filter=not_paid")
|
||||||
|
|
||||||
|
refute html =~ paid_member.first_name
|
||||||
|
assert html =~ unpaid_member.first_name
|
||||||
|
assert html =~ nil_paid_member.first_name
|
||||||
|
end
|
||||||
|
|
||||||
|
test "filter combines with search query (AND)", %{
|
||||||
|
conn: conn,
|
||||||
|
paid_member: paid_member
|
||||||
|
} do
|
||||||
|
conn = conn_with_oidc_user(conn)
|
||||||
|
{:ok, _view, html} = live(conn, "/members?query=Zahler&paid_filter=paid")
|
||||||
|
|
||||||
|
assert html =~ paid_member.first_name
|
||||||
|
end
|
||||||
|
|
||||||
|
test "filter combines with sorting", %{conn: conn} do
|
||||||
|
conn = conn_with_oidc_user(conn)
|
||||||
|
|
||||||
|
{:ok, view, _html} =
|
||||||
|
live(conn, "/members?paid_filter=paid&sort_field=first_name&sort_order=asc")
|
||||||
|
|
||||||
|
# Click on email sort header
|
||||||
|
view
|
||||||
|
|> element("[data-testid='email']")
|
||||||
|
|> render_click()
|
||||||
|
|
||||||
|
# Filter should be preserved in URL
|
||||||
|
path = assert_patch(view)
|
||||||
|
assert path =~ "paid_filter=paid"
|
||||||
|
assert path =~ "sort_field=email"
|
||||||
|
end
|
||||||
|
|
||||||
|
test "URL parameter paid_filter is set when selecting filter", %{conn: conn} do
|
||||||
|
conn = conn_with_oidc_user(conn)
|
||||||
|
{:ok, view, _html} = live(conn, "/members")
|
||||||
|
|
||||||
|
# Open filter dropdown
|
||||||
|
view
|
||||||
|
|> element("#payment-filter button[aria-haspopup='true']")
|
||||||
|
|> render_click()
|
||||||
|
|
||||||
|
# Select "Paid" option
|
||||||
|
view
|
||||||
|
|> element("#payment-filter button[phx-value-filter='paid']")
|
||||||
|
|> render_click()
|
||||||
|
|
||||||
|
path = assert_patch(view)
|
||||||
|
assert path =~ "paid_filter=paid"
|
||||||
|
end
|
||||||
|
|
||||||
|
test "URL parameter is correctly read on page load", %{
|
||||||
|
conn: conn,
|
||||||
|
paid_member: paid_member
|
||||||
|
} do
|
||||||
|
conn = conn_with_oidc_user(conn)
|
||||||
|
{:ok, _view, html} = live(conn, "/members?paid_filter=paid")
|
||||||
|
|
||||||
|
# Only paid member should be visible
|
||||||
|
assert html =~ paid_member.first_name
|
||||||
|
# Filter badge should be visible
|
||||||
|
assert html =~ "badge"
|
||||||
|
end
|
||||||
|
|
||||||
|
test "invalid URL parameter is ignored", %{
|
||||||
|
conn: conn,
|
||||||
|
paid_member: paid_member,
|
||||||
|
unpaid_member: unpaid_member
|
||||||
|
} do
|
||||||
|
conn = conn_with_oidc_user(conn)
|
||||||
|
{:ok, _view, html} = live(conn, "/members?paid_filter=invalid_value")
|
||||||
|
|
||||||
|
# All members should be visible (filter not applied)
|
||||||
|
assert html =~ paid_member.first_name
|
||||||
|
assert html =~ unpaid_member.first_name
|
||||||
|
end
|
||||||
|
|
||||||
|
test "search maintains filter state", %{conn: conn} do
|
||||||
|
conn = conn_with_oidc_user(conn)
|
||||||
|
{:ok, view, _html} = live(conn, "/members?paid_filter=paid")
|
||||||
|
|
||||||
|
# Perform search
|
||||||
|
view
|
||||||
|
|> element("[data-testid='search-input']")
|
||||||
|
|> render_change(%{"query" => "test"})
|
||||||
|
|
||||||
|
# Filter state should be maintained in URL
|
||||||
|
path = assert_patch(view)
|
||||||
|
assert path =~ "paid_filter=paid"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "paid column in table" do
|
||||||
|
setup do
|
||||||
|
{:ok, paid_member} =
|
||||||
|
Mv.Membership.create_member(%{
|
||||||
|
first_name: "Paid",
|
||||||
|
last_name: "Member",
|
||||||
|
email: "paid.column@example.com",
|
||||||
|
paid: true
|
||||||
|
})
|
||||||
|
|
||||||
|
{:ok, unpaid_member} =
|
||||||
|
Mv.Membership.create_member(%{
|
||||||
|
first_name: "Unpaid",
|
||||||
|
last_name: "Member",
|
||||||
|
email: "unpaid.column@example.com",
|
||||||
|
paid: false
|
||||||
|
})
|
||||||
|
|
||||||
|
%{paid_member: paid_member, unpaid_member: unpaid_member}
|
||||||
|
end
|
||||||
|
|
||||||
|
test "paid column shows green badge for paid members", %{conn: conn} do
|
||||||
|
conn = conn_with_oidc_user(conn)
|
||||||
|
{:ok, _view, html} = live(conn, "/members")
|
||||||
|
|
||||||
|
# Check for success badge (green)
|
||||||
|
assert html =~ "badge-success"
|
||||||
|
end
|
||||||
|
|
||||||
|
test "paid column shows red badge for unpaid members", %{conn: conn} do
|
||||||
|
conn = conn_with_oidc_user(conn)
|
||||||
|
{:ok, _view, html} = live(conn, "/members")
|
||||||
|
|
||||||
|
# Check for error badge (red)
|
||||||
|
assert html =~ "badge-error"
|
||||||
|
end
|
||||||
|
|
||||||
|
test "paid column shows 'Yes' for paid members", %{conn: conn} do
|
||||||
|
conn = conn_with_oidc_user(conn)
|
||||||
|
Gettext.put_locale(MvWeb.Gettext, "en")
|
||||||
|
{:ok, _view, html} = live(conn, "/members")
|
||||||
|
|
||||||
|
# The table should contain "Yes" text inside badge
|
||||||
|
assert html =~ "badge-success"
|
||||||
|
assert html =~ "Yes"
|
||||||
|
end
|
||||||
|
|
||||||
|
test "paid column shows 'No' for unpaid members", %{conn: conn} do
|
||||||
|
conn = conn_with_oidc_user(conn)
|
||||||
|
Gettext.put_locale(MvWeb.Gettext, "en")
|
||||||
|
{:ok, _view, html} = live(conn, "/members")
|
||||||
|
|
||||||
|
# The table should contain "No" text inside badge
|
||||||
|
assert html =~ "badge-error"
|
||||||
|
assert html =~ "No"
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue