diff --git a/docs/database-schema-readme.md b/docs/database-schema-readme.md
index 1644f2a..d548b82 100644
--- a/docs/database-schema-readme.md
+++ b/docs/database-schema-readme.md
@@ -115,6 +115,7 @@ Member (1) → (N) Properties
### Member Constraints
- First name and last name required (min 1 char)
- Email unique, validated format (5-254 chars)
+- Birth date cannot be in future
- Join date cannot be in future
- Exit date must be after join date
- Phone: `+?[0-9\- ]{6,20}`
@@ -168,7 +169,7 @@ Member (1) → (N) Properties
### Weighted Fields
- **Weight A (highest):** first_name, last_name
- **Weight B:** email, notes
-- **Weight C:** phone_number, city, street, house_number, postal_code
+- **Weight C:** birth_date, phone_number, city, street, house_number, postal_code
- **Weight D (lowest):** join_date, exit_date
### Usage Example
@@ -380,7 +381,7 @@ Install "DBML Language" extension to view/edit DBML files with:
- tokens (jti, purpose, extra_data)
**Personal Data (GDPR):**
-- All member fields (name, email, address)
+- All member fields (name, email, birth_date, address)
- User email
- Token subject
diff --git a/docs/database_schema.dbml b/docs/database_schema.dbml
index b620830..33c0647 100644
--- a/docs/database_schema.dbml
+++ b/docs/database_schema.dbml
@@ -122,6 +122,7 @@ Table members {
first_name text [not null, note: 'Member first name (min length: 1)']
last_name text [not null, note: 'Member last name (min length: 1)']
email text [not null, unique, note: 'Member email address (5-254 chars, validated)']
+ birth_date date [null, note: 'Date of birth (cannot be in future)']
paid boolean [null, note: 'Payment status flag']
phone_number text [null, note: 'Contact phone number (format: +?[0-9\- ]{6,20})']
join_date date [null, note: 'Date when member joined club (cannot be in future)']
@@ -152,7 +153,7 @@ Table members {
**Club Member Master Data**
Core entity for membership management containing:
- - Personal information (name, email)
+ - Personal information (name, birth date, email)
- Contact details (phone, address)
- Membership status (join/exit dates, payment status)
- Additional notes
@@ -182,6 +183,7 @@ Table members {
**Validation Rules:**
- first_name, last_name: min 1 character
- email: 5-254 characters, valid email format
+ - birth_date: cannot be in future
- join_date: cannot be in future
- exit_date: must be after join_date (if both present)
- phone_number: matches pattern ^\+?[0-9\- ]{6,20}$
diff --git a/docs/feature-roadmap.md b/docs/feature-roadmap.md
index 2f86f5e..9758f07 100644
--- a/docs/feature-roadmap.md
+++ b/docs/feature-roadmap.md
@@ -100,10 +100,10 @@
**Closed Issues:**
- [#194](https://git.local-it.org/local-it/mitgliederverwaltung/issues/194) - Custom Fields: Harden implementation (S)
- [#197](https://git.local-it.org/local-it/mitgliederverwaltung/issues/197) - Custom Fields: Add option to show custom fields in member overview (M)
-- [#161](https://git.local-it.org/local-it/mitgliederverwaltung/issues/161) - Remove birthday field from default configuration (S) - Closed 2025-12-02
**Open Issues:**
- [#157](https://git.local-it.org/local-it/mitgliederverwaltung/issues/157) - Concept how custom fields are handled (M, High priority) [0/4 tasks]
+- [#161](https://git.local-it.org/local-it/mitgliederverwaltung/issues/161) - Don't show birthday field for default configurations (S, Low priority)
- [#153](https://git.local-it.org/local-it/mitgliederverwaltung/issues/153) - Sorting functionalities for custom fields (M, Low priority)
**Missing Features:**
diff --git a/lib/membership/member.ex b/lib/membership/member.ex
index 8d271d7..bcd505e 100644
--- a/lib/membership/member.ex
+++ b/lib/membership/member.ex
@@ -24,7 +24,7 @@ defmodule Mv.Membership.Member do
- Email format validation (using EctoCommons.EmailValidator)
- Phone number format: international format with 6-20 digits
- Postal code format: exactly 5 digits (German format)
- - Date validations: join_date not in future, exit_date after join_date
+ - Date validations: birth_date and join_date not in future, exit_date after join_date
- Email uniqueness: prevents conflicts with unlinked users
## Full-Text Search
@@ -284,6 +284,11 @@ defmodule Mv.Membership.Member do
end
end
+ # Birth date not in the future
+ validate compare(:birth_date, less_than_or_equal_to: &Date.utc_today/0),
+ where: [present(:birth_date)],
+ message: "cannot be in the future"
+
# Join date not in the future
validate compare(:join_date, less_than_or_equal_to: &Date.utc_today/0),
where: [present(:join_date)],
@@ -346,6 +351,10 @@ defmodule Mv.Membership.Member do
constraints min_length: 5, max_length: 254
end
+ attribute :birth_date, :date do
+ allow_nil? true
+ end
+
attribute :paid, :boolean do
allow_nil? true
end
diff --git a/lib/mv/constants.ex b/lib/mv/constants.ex
index 334bcc1..cd8d3a4 100644
--- a/lib/mv/constants.ex
+++ b/lib/mv/constants.ex
@@ -7,6 +7,7 @@ defmodule Mv.Constants do
:first_name,
:last_name,
:email,
+ :birth_date,
:paid,
:phone_number,
:join_date,
diff --git a/lib/mv_web/live/components/payment_filter_component.ex b/lib/mv_web/live/components/payment_filter_component.ex
deleted file mode 100644
index c9dc731..0000000
--- a/lib/mv_web/live/components/payment_filter_component.ex
+++ /dev/null
@@ -1,146 +0,0 @@
-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"""
-
-
-
-
- -
-
-
- -
-
-
- -
-
-
-
-
- """
- 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
diff --git a/lib/mv_web/live/member_live/form.ex b/lib/mv_web/live/member_live/form.ex
index 5370154..8d8863f 100644
--- a/lib/mv_web/live/member_live/form.ex
+++ b/lib/mv_web/live/member_live/form.ex
@@ -14,7 +14,7 @@ defmodule MvWeb.MemberLive.Form do
- first_name, last_name, email
**Optional:**
- - phone_number, address fields (city, street, house_number, postal_code)
+ - birth_date, phone_number, address fields (city, street, house_number, postal_code)
- join_date, exit_date
- paid status
- notes
@@ -45,6 +45,7 @@ defmodule MvWeb.MemberLive.Form do
<.input field={@form[:first_name]} label={gettext("First Name")} required />
<.input field={@form[:last_name]} label={gettext("Last Name")} required />
<.input field={@form[:email]} label={gettext("Email")} required type="email" />
+ <.input field={@form[:birth_date]} label={gettext("Birth Date")} type="date" />
<.input field={@form[:paid]} label={gettext("Paid")} type="checkbox" />
<.input field={@form[:phone_number]} label={gettext("Phone Number")} />
<.input field={@form[:join_date]} label={gettext("Join Date")} type="date" />
diff --git a/lib/mv_web/live/member_live/index.ex b/lib/mv_web/live/member_live/index.ex
index 3d30d76..4d444b9 100644
--- a/lib/mv_web/live/member_live/index.ex
+++ b/lib/mv_web/live/member_live/index.ex
@@ -46,7 +46,7 @@ defmodule MvWeb.MemberLive.Index do
Initializes the LiveView state.
Sets up initial assigns for page title, search query, sort configuration,
- payment filter, and member selection. Actual data loading happens in `handle_params/3`.
+ and member selection. Actual data loading happens in `handle_params/3`.
"""
@impl true
def mount(_params, _session, socket) do
@@ -74,7 +74,6 @@ defmodule MvWeb.MemberLive.Index do
|> assign(:query, "")
|> assign_new(:sort_field, fn -> :first_name end)
|> assign_new(:sort_order, fn -> :asc end)
- |> assign(:paid_filter, nil)
|> assign(:selected_members, MapSet.new())
|> assign(:custom_fields_visible, custom_fields_visible)
|> assign(:member_fields_visible, get_visible_member_fields(settings))
@@ -208,17 +207,17 @@ defmodule MvWeb.MemberLive.Index do
@impl true
def handle_info({:search_changed, q}, socket) do
- socket =
- socket
- |> assign(:query, q)
- |> load_members()
+ socket = load_members(socket, q)
existing_field_query = socket.assigns.sort_field
existing_sort_query = socket.assigns.sort_order
# Build the URL with queries
- query_params =
- build_query_params(q, existing_field_query, existing_sort_query, socket.assigns.paid_filter)
+ query_params = %{
+ "query" => q,
+ "sort_field" => existing_field_query,
+ "sort_order" => existing_sort_query
+ }
# Set the new path with params
new_path = ~p"/members?#{query_params}"
@@ -231,38 +230,13 @@ defmodule MvWeb.MemberLive.Index do
)}
end
- @impl true
- def handle_info({:payment_filter_changed, filter}, socket) do
- socket =
- socket
- |> assign(:paid_filter, filter)
- |> load_members()
-
- # 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
# -----------------------------------------------------------------
@doc """
Handles URL parameter changes.
- Parses query parameters for search query, sort field, sort order, and payment filter,
+ Parses query parameters for search query, sort field, and sort order,
then loads members accordingly. This enables bookmarkable URLs and
browser back/forward navigation.
"""
@@ -272,9 +246,7 @@ defmodule MvWeb.MemberLive.Index do
socket
|> maybe_update_search(params)
|> maybe_update_sort(params)
- |> maybe_update_paid_filter(params)
- |> assign(:query, params["query"])
- |> load_members()
+ |> load_members(params["query"])
|> prepare_dynamic_cols()
{:noreply, socket}
@@ -365,13 +337,11 @@ defmodule MvWeb.MemberLive.Index do
field
end
- query_params =
- build_query_params(
- socket.assigns.query,
- field_str,
- Atom.to_string(order),
- socket.assigns.paid_filter
- )
+ query_params = %{
+ "query" => socket.assigns.query,
+ "sort_field" => field_str,
+ "sort_order" => Atom.to_string(order)
+ }
new_path = ~p"/members?#{query_params}"
@@ -382,45 +352,13 @@ defmodule MvWeb.MemberLive.Index do
)}
end
- # 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.
+ # Loads members from the database with custom field values and applies search/sort filters.
#
# Process:
# 1. Builds base query with selected fields
# 2. Loads custom field values for visible custom fields (filtered at database level)
# 3. Applies search filter if provided
- # 4. Applies payment status filter if set
- # 5. Applies sorting (database-level for regular fields, in-memory for custom fields)
+ # 4. Applies sorting (database-level for regular fields, in-memory for custom fields)
#
# Performance Considerations:
# - Database-level filtering: Custom field values are filtered directly in the database
@@ -432,9 +370,7 @@ defmodule MvWeb.MemberLive.Index do
# consider implementing pagination (see Issue #165).
#
# Returns the socket with `:members` assigned.
- defp load_members(socket) do
- search_query = socket.assigns.query
-
+ defp load_members(socket, search_query) do
query =
Mv.Membership.Member
|> Ash.Query.new()
@@ -447,9 +383,6 @@ defmodule MvWeb.MemberLive.Index do
# Apply the search filter first
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
# For custom fields, we sort after loading
{query, sort_after_load} =
@@ -524,24 +457,6 @@ defmodule MvWeb.MemberLive.Index do
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
defp toggle_order(:asc), do: :desc
defp toggle_order(:desc), do: :asc
@@ -832,29 +747,6 @@ defmodule MvWeb.MemberLive.Index do
socket
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.
- #
- # SECURITY: This function whitelists allowed filter values. Only "paid" and "not_paid"
- # are accepted - all other input (including malicious strings) falls back to nil.
- # This ensures no raw user input is ever passed to Ash.Query.filter/2, following
- # Ash's security recommendation to never pass untrusted input directly to filters.
- 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
# -------------------------------------------------------------
diff --git a/lib/mv_web/live/member_live/index.html.heex b/lib/mv_web/live/member_live/index.html.heex
index 58e22b6..55b0a20 100644
--- a/lib/mv_web/live/member_live/index.html.heex
+++ b/lib/mv_web/live/member_live/index.html.heex
@@ -26,20 +26,12 @@
-
- <.live_component
- module={MvWeb.Components.SearchBarComponent}
- id="search-bar"
- query={@query}
- placeholder={gettext("Search...")}
- />
- <.live_component
- module={MvWeb.Components.PaymentFilterComponent}
- id="payment-filter"
- paid_filter={@paid_filter}
- member_count={length(@members)}
- />
-
+ <.live_component
+ module={MvWeb.Components.SearchBarComponent}
+ id="search-bar"
+ query={@query}
+ placeholder={gettext("Search...")}
+ />
<.table
id="members"
@@ -221,14 +213,6 @@
>
{member.join_date}
- <:col :let={member} label={gettext("Paid")}>
-
- {if member.paid == true, do: gettext("Yes"), else: gettext("No")}
-
-
<:action :let={member}>
<.link navigate={~p"/members/#{member}"}>{gettext("Show")}
diff --git a/lib/mv_web/live/member_live/show.ex b/lib/mv_web/live/member_live/show.ex
index de46a3a..7ec24fa 100644
--- a/lib/mv_web/live/member_live/show.ex
+++ b/lib/mv_web/live/member_live/show.ex
@@ -10,7 +10,7 @@ defmodule MvWeb.MemberLive.Show do
- Return to member list
## Displayed Information
- - Basic: name, email, dates (join, exit)
+ - Basic: name, email, dates (birth, join, exit)
- Contact: phone number
- Address: street, house number, postal code, city
- Status: paid flag
@@ -48,6 +48,7 @@ defmodule MvWeb.MemberLive.Show do
<:item title={gettext("First Name")}>{@member.first_name}
<:item title={gettext("Last Name")}>{@member.last_name}
<:item title={gettext("Email")}>{@member.email}
+ <:item title={gettext("Birth Date")}>{@member.birth_date}
<:item title={gettext("Paid")}>
{if @member.paid, do: gettext("Yes"), else: gettext("No")}
diff --git a/priv/gettext/de/LC_MESSAGES/default.po b/priv/gettext/de/LC_MESSAGES/default.po
index f7dd49e..c218f48 100644
--- a/priv/gettext/de/LC_MESSAGES/default.po
+++ b/priv/gettext/de/LC_MESSAGES/default.po
@@ -16,7 +16,7 @@ msgstr ""
msgid "Actions"
msgstr "Aktionen"
-#: lib/mv_web/live/member_live/index.html.heex:243
+#: lib/mv_web/live/member_live/index.html.heex:227
#: lib/mv_web/live/user_live/index.html.heex:72
#, elixir-autogen, elixir-format
msgid "Are you sure?"
@@ -28,22 +28,22 @@ msgstr "Bist du sicher?"
msgid "Attempting to reconnect"
msgstr "Verbindung wird wiederhergestellt"
-#: lib/mv_web/live/member_live/form.ex:53
-#: lib/mv_web/live/member_live/index.html.heex:179
-#: lib/mv_web/live/member_live/show.ex:58
+#: 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/show.ex:59
#, elixir-autogen, elixir-format
msgid "City"
msgstr "Stadt"
#: lib/mv_web/live/contribution_type_live/index.ex:78
-#: lib/mv_web/live/member_live/index.html.heex:245
+#: lib/mv_web/live/member_live/index.html.heex:229
#: lib/mv_web/live/user_live/index.html.heex:74
#, elixir-autogen, elixir-format
msgid "Delete"
msgstr "Löschen"
#: lib/mv_web/live/contribution_type_live/index.ex:66
-#: lib/mv_web/live/member_live/index.html.heex:237
+#: lib/mv_web/live/member_live/index.html.heex:221
#: lib/mv_web/live/user_live/form.ex:265
#: lib/mv_web/live/user_live/index.html.heex:66
#, elixir-autogen, elixir-format
@@ -51,14 +51,14 @@ msgid "Edit"
msgstr "Bearbeite"
#: lib/mv_web/live/member_live/show.ex:41
-#: lib/mv_web/live/member_live/show.ex:116
+#: lib/mv_web/live/member_live/show.ex:117
#, elixir-autogen, elixir-format
msgid "Edit Member"
msgstr "Mitglied bearbeiten"
#: lib/mv_web/live/contribution_period_live/show.ex:58
#: lib/mv_web/live/member_live/form.ex:47
-#: lib/mv_web/live/member_live/index.html.heex:107
+#: lib/mv_web/live/member_live/index.html.heex:99
#: lib/mv_web/live/member_live/show.ex:50
#: lib/mv_web/live/user_live/form.ex:46
#: lib/mv_web/live/user_live/index.html.heex:44
@@ -73,9 +73,9 @@ msgstr "E-Mail"
msgid "First Name"
msgstr "Vorname"
-#: lib/mv_web/live/member_live/form.ex:50
-#: lib/mv_web/live/member_live/index.html.heex:215
-#: lib/mv_web/live/member_live/show.ex:55
+#: 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/show.ex:56
#, elixir-autogen, elixir-format
msgid "Join Date"
msgstr "Beitrittsdatum"
@@ -91,7 +91,7 @@ msgstr "Nachname"
msgid "New Member"
msgstr "Neues Mitglied"
-#: lib/mv_web/live/member_live/index.html.heex:234
+#: lib/mv_web/live/member_live/index.html.heex:218
#: lib/mv_web/live/user_live/index.html.heex:63
#, elixir-autogen, elixir-format
msgid "Show"
@@ -112,52 +112,55 @@ msgstr "Keine Internetverbindung gefunden"
msgid "close"
msgstr "schließen"
-#: lib/mv_web/live/member_live/form.ex:51
-#: lib/mv_web/live/member_live/show.ex:56
+#: lib/mv_web/live/member_live/form.ex:48
+#: lib/mv_web/live/member_live/show.ex:51
+#, elixir-autogen, elixir-format
+msgid "Birth Date"
+msgstr "Geburtsdatum"
+
+#: lib/mv_web/live/member_live/form.ex:52
+#: lib/mv_web/live/member_live/show.ex:57
#, elixir-autogen, elixir-format
msgid "Exit Date"
msgstr "Austrittsdatum"
-#: lib/mv_web/live/member_live/form.ex:55
-#: lib/mv_web/live/member_live/index.html.heex:143
-#: lib/mv_web/live/member_live/show.ex:60
+#: 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/show.ex:61
#, elixir-autogen, elixir-format
msgid "House Number"
msgstr "Hausnummer"
#: lib/mv_web/live/contribution_period_live/show.ex:140
-#: lib/mv_web/live/member_live/form.ex:52
-#: lib/mv_web/live/member_live/show.ex:57
+#: lib/mv_web/live/member_live/form.ex:53
+#: lib/mv_web/live/member_live/show.ex:58
#, elixir-autogen, elixir-format
msgid "Notes"
msgstr "Notizen"
-#: lib/mv_web/live/components/payment_filter_component.ex:94
-#: lib/mv_web/live/components/payment_filter_component.ex:144
#: lib/mv_web/live/contribution_period_live/show.ex:186
#: lib/mv_web/live/contribution_period_live/show.ex:243
-#: lib/mv_web/live/member_live/form.ex:48
-#: lib/mv_web/live/member_live/index.html.heex:224
-#: lib/mv_web/live/member_live/show.ex:51
+#: lib/mv_web/live/member_live/form.ex:49
+#: lib/mv_web/live/member_live/show.ex:52
#, elixir-autogen, elixir-format
msgid "Paid"
msgstr "Bezahlt"
-#: lib/mv_web/live/member_live/form.ex:49
-#: lib/mv_web/live/member_live/index.html.heex:197
-#: lib/mv_web/live/member_live/show.ex:54
+#: 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/show.ex:55
#, elixir-autogen, elixir-format
msgid "Phone Number"
msgstr "Telefonnummer"
-#: lib/mv_web/live/member_live/form.ex:56
-#: lib/mv_web/live/member_live/index.html.heex:161
-#: lib/mv_web/live/member_live/show.ex:61
+#: 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/show.ex:62
#, elixir-autogen, elixir-format
msgid "Postal Code"
msgstr "Postleitzahl"
-#: lib/mv_web/live/member_live/form.ex:79
+#: lib/mv_web/live/member_live/form.ex:80
#, elixir-autogen, elixir-format
msgid "Save Member"
msgstr "Mitglied speichern"
@@ -165,15 +168,15 @@ msgstr "Mitglied speichern"
#: lib/mv_web/live/custom_field_live/form.ex:66
#: lib/mv_web/live/custom_field_value_live/form.ex:74
#: lib/mv_web/live/global_settings_live.ex:55
-#: lib/mv_web/live/member_live/form.ex:78
+#: lib/mv_web/live/member_live/form.ex:79
#: lib/mv_web/live/user_live/form.ex:248
#, elixir-autogen, elixir-format
msgid "Saving..."
msgstr "Speichern..."
-#: lib/mv_web/live/member_live/form.ex:54
-#: lib/mv_web/live/member_live/index.html.heex:125
-#: lib/mv_web/live/member_live/show.ex:59
+#: 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/show.ex:60
#, elixir-autogen, elixir-format
msgid "Street"
msgstr "Straße"
@@ -183,14 +186,13 @@ msgstr "Straße"
msgid "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/show.ex:52
+#: lib/mv_web/live/member_live/show.ex:53
#, elixir-autogen, elixir-format
msgid "No"
msgstr "Nein"
-#: lib/mv_web/live/member_live/show.ex:115
+#: lib/mv_web/live/member_live/show.ex:116
#, elixir-autogen, elixir-format, fuzzy
msgid "Show Member"
msgstr "Mitglied anzeigen"
@@ -200,23 +202,22 @@ msgstr "Mitglied anzeigen"
msgid "This is a member record from your database."
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/show.ex:52
+#: lib/mv_web/live/member_live/show.ex:53
#, elixir-autogen, elixir-format
msgid "Yes"
msgstr "Ja"
#: lib/mv_web/live/custom_field_live/form.ex:110
#: lib/mv_web/live/custom_field_value_live/form.ex:233
-#: lib/mv_web/live/member_live/form.ex:137
+#: lib/mv_web/live/member_live/form.ex:138
#, elixir-autogen, elixir-format
msgid "create"
msgstr "erstellt"
#: lib/mv_web/live/custom_field_live/form.ex:111
#: lib/mv_web/live/custom_field_value_live/form.ex:234
-#: lib/mv_web/live/member_live/form.ex:138
+#: lib/mv_web/live/member_live/form.ex:139
#, elixir-autogen, elixir-format
msgid "update"
msgstr "aktualisiert"
@@ -226,7 +227,7 @@ msgstr "aktualisiert"
msgid "Incorrect email or password"
msgstr "Falsche E-Mail oder Passwort"
-#: lib/mv_web/live/member_live/form.ex:144
+#: lib/mv_web/live/member_live/form.ex:145
#, elixir-autogen, elixir-format
msgid "Member %{action} successfully"
msgstr "Mitglied %{action} erfolgreich"
@@ -259,7 +260,7 @@ msgstr "Ihr Passwort wurde erfolgreich zurückgesetzt"
#: lib/mv_web/live/custom_field_live/form.ex:69
#: lib/mv_web/live/custom_field_live/index.ex:120
#: lib/mv_web/live/custom_field_value_live/form.ex:77
-#: lib/mv_web/live/member_live/form.ex:81
+#: lib/mv_web/live/member_live/form.ex:82
#: lib/mv_web/live/user_live/form.ex:251
#, elixir-autogen, elixir-format
msgid "Cancel"
@@ -370,12 +371,12 @@ msgstr "Profil"
msgid "Required"
msgstr "Erforderlich"
-#: lib/mv_web/live/member_live/index.html.heex:63
+#: lib/mv_web/live/member_live/index.html.heex:55
#, elixir-autogen, elixir-format
msgid "Select all members"
msgstr "Alle Mitglieder auswählen"
-#: lib/mv_web/live/member_live/index.html.heex:77
+#: lib/mv_web/live/member_live/index.html.heex:69
#, elixir-autogen, elixir-format
msgid "Select member"
msgstr "Mitglied auswählen"
@@ -520,7 +521,7 @@ msgstr "Benutzer*in wird ohne Passwort erstellt. Aktivieren Sie 'Passwort setzen
msgid "Linked Member"
msgstr "Verknüpftes Mitglied"
-#: lib/mv_web/live/member_live/show.ex:62
+#: lib/mv_web/live/member_live/show.ex:63
#, elixir-autogen, elixir-format
msgid "Linked User"
msgstr "Verknüpfte*r Benutzer*in"
@@ -531,7 +532,7 @@ msgstr "Verknüpfte*r Benutzer*in"
msgid "No member linked"
msgstr "Kein Mitglied verknüpft"
-#: lib/mv_web/live/member_live/show.ex:72
+#: lib/mv_web/live/member_live/show.ex:73
#, elixir-autogen, elixir-format
msgid "No user linked"
msgstr "Keine*r Benutzer*in verknüpft"
@@ -561,7 +562,7 @@ msgid "Toggle dark mode"
msgstr "Dunklen Modus umschalten"
#: lib/mv_web/live/components/search_bar_component.ex:15
-#: lib/mv_web/live/member_live/index.html.heex:34
+#: lib/mv_web/live/member_live/index.html.heex:33
#, elixir-autogen, elixir-format
msgid "Search..."
msgstr "Suchen..."
@@ -577,7 +578,7 @@ msgstr "Benutzer*innen"
msgid "Click to sort"
msgstr "Klicke um zu sortieren"
-#: lib/mv_web/live/member_live/index.html.heex:89
+#: lib/mv_web/live/member_live/index.html.heex:81
#, elixir-autogen, elixir-format
msgid "First name"
msgstr "Vorname"
@@ -618,8 +619,8 @@ msgstr "Diese E-Mail-Adresse ist bereits mit einem anderen OIDC-Konto verknüpft
msgid "Choose a custom field"
msgstr "Wähle ein Benutzerdefiniertes Feld"
-#: lib/mv_web/live/member_live/form.ex:58
-#: lib/mv_web/live/member_live/show.ex:77
+#: lib/mv_web/live/member_live/form.ex:59
+#: lib/mv_web/live/member_live/show.ex:78
#, elixir-autogen, elixir-format
msgid "Custom Field Values"
msgstr "Benutzerdefinierte Feldwerte"
@@ -789,7 +790,7 @@ msgstr "Mitglied entverknüpfen"
msgid "Unlinking scheduled"
msgstr "Entverknüpfung geplant"
-#: lib/mv_web/live/member_live/index.ex:165
+#: lib/mv_web/live/member_live/index.ex:164
#, elixir-autogen, elixir-format
msgid "Copied %{count} email address to clipboard"
msgid_plural "Copied %{count} email addresses to clipboard"
@@ -806,12 +807,12 @@ msgstr "E-Mail-Adressen der ausgewählten Mitglieder kopieren"
msgid "Copy emails"
msgstr "E-Mails kopieren"
-#: lib/mv_web/live/member_live/index.ex:154
+#: lib/mv_web/live/member_live/index.ex:153
#, elixir-autogen, elixir-format
msgid "No email addresses found"
msgstr "Keine E-Mail-Adressen gefunden"
-#: lib/mv_web/live/member_live/index.ex:151
+#: lib/mv_web/live/member_live/index.ex:150
#, elixir-autogen, elixir-format
msgid "No members selected"
msgstr "Keine Mitglieder ausgewählt"
@@ -826,7 +827,7 @@ msgstr "E-Mail-Programm mit BCC-Empfänger*innen öffnen"
msgid "Open in email program"
msgstr "Im E-Mail-Programm öffnen"
-#: lib/mv_web/live/member_live/index.ex:174
+#: lib/mv_web/live/member_live/index.ex:173
#, elixir-autogen, elixir-format
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"
@@ -844,28 +845,6 @@ msgstr "Felder, die mit einem Sternchen (*) markiert sind, dürfen nicht leer bl
msgid "This field cannot be empty"
msgstr "Dieses Feld darf nicht leer bleiben"
-#: lib/mv_web/live/components/payment_filter_component.ex:80
-#: lib/mv_web/live/components/payment_filter_component.ex:143
-#, elixir-autogen, elixir-format
-msgid "All"
-msgstr "Alle"
-
-#: lib/mv_web/live/components/payment_filter_component.ex:54
-#, elixir-autogen, elixir-format
-msgid "Filter by payment status"
-msgstr "Nach Zahlungsstatus filtern"
-
-#: lib/mv_web/live/components/payment_filter_component.ex:108
-#: lib/mv_web/live/components/payment_filter_component.ex:145
-#, elixir-autogen, elixir-format
-msgid "Not paid"
-msgstr "Nicht bezahlt"
-
-#: lib/mv_web/live/components/payment_filter_component.ex:65
-#, elixir-autogen, elixir-format
-msgid "Payment filter"
-msgstr "Zahlungsfilter"
-
#: lib/mv_web/live/contribution_type_live/index.ex:113
#, elixir-autogen, elixir-format
msgid "About Contribution Types"
diff --git a/priv/gettext/default.pot b/priv/gettext/default.pot
index fb309dd..68c011d 100644
--- a/priv/gettext/default.pot
+++ b/priv/gettext/default.pot
@@ -17,7 +17,7 @@ msgstr ""
msgid "Actions"
msgstr ""
-#: lib/mv_web/live/member_live/index.html.heex:243
+#: lib/mv_web/live/member_live/index.html.heex:227
#: lib/mv_web/live/user_live/index.html.heex:72
#, elixir-autogen, elixir-format
msgid "Are you sure?"
@@ -29,22 +29,22 @@ msgstr ""
msgid "Attempting to reconnect"
msgstr ""
-#: lib/mv_web/live/member_live/form.ex:53
-#: lib/mv_web/live/member_live/index.html.heex:179
-#: lib/mv_web/live/member_live/show.ex:58
+#: 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/show.ex:59
#, elixir-autogen, elixir-format
msgid "City"
msgstr ""
#: lib/mv_web/live/contribution_type_live/index.ex:78
-#: lib/mv_web/live/member_live/index.html.heex:245
+#: lib/mv_web/live/member_live/index.html.heex:229
#: lib/mv_web/live/user_live/index.html.heex:74
#, elixir-autogen, elixir-format
msgid "Delete"
msgstr ""
#: lib/mv_web/live/contribution_type_live/index.ex:66
-#: lib/mv_web/live/member_live/index.html.heex:237
+#: lib/mv_web/live/member_live/index.html.heex:221
#: lib/mv_web/live/user_live/form.ex:265
#: lib/mv_web/live/user_live/index.html.heex:66
#, elixir-autogen, elixir-format
@@ -52,14 +52,14 @@ msgid "Edit"
msgstr ""
#: lib/mv_web/live/member_live/show.ex:41
-#: lib/mv_web/live/member_live/show.ex:116
+#: lib/mv_web/live/member_live/show.ex:117
#, elixir-autogen, elixir-format
msgid "Edit Member"
msgstr ""
#: lib/mv_web/live/contribution_period_live/show.ex:58
#: lib/mv_web/live/member_live/form.ex:47
-#: lib/mv_web/live/member_live/index.html.heex:107
+#: lib/mv_web/live/member_live/index.html.heex:99
#: lib/mv_web/live/member_live/show.ex:50
#: lib/mv_web/live/user_live/form.ex:46
#: lib/mv_web/live/user_live/index.html.heex:44
@@ -74,9 +74,9 @@ msgstr ""
msgid "First Name"
msgstr ""
-#: lib/mv_web/live/member_live/form.ex:50
-#: lib/mv_web/live/member_live/index.html.heex:215
-#: lib/mv_web/live/member_live/show.ex:55
+#: 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/show.ex:56
#, elixir-autogen, elixir-format
msgid "Join Date"
msgstr ""
@@ -92,7 +92,7 @@ msgstr ""
msgid "New Member"
msgstr ""
-#: lib/mv_web/live/member_live/index.html.heex:234
+#: lib/mv_web/live/member_live/index.html.heex:218
#: lib/mv_web/live/user_live/index.html.heex:63
#, elixir-autogen, elixir-format
msgid "Show"
@@ -113,52 +113,55 @@ msgstr ""
msgid "close"
msgstr ""
-#: lib/mv_web/live/member_live/form.ex:51
-#: lib/mv_web/live/member_live/show.ex:56
+#: lib/mv_web/live/member_live/form.ex:48
+#: lib/mv_web/live/member_live/show.ex:51
+#, elixir-autogen, elixir-format
+msgid "Birth Date"
+msgstr ""
+
+#: lib/mv_web/live/member_live/form.ex:52
+#: lib/mv_web/live/member_live/show.ex:57
#, elixir-autogen, elixir-format
msgid "Exit Date"
msgstr ""
-#: lib/mv_web/live/member_live/form.ex:55
-#: lib/mv_web/live/member_live/index.html.heex:143
-#: lib/mv_web/live/member_live/show.ex:60
+#: 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/show.ex:61
#, elixir-autogen, elixir-format
msgid "House Number"
msgstr ""
#: lib/mv_web/live/contribution_period_live/show.ex:140
-#: lib/mv_web/live/member_live/form.ex:52
-#: lib/mv_web/live/member_live/show.ex:57
+#: lib/mv_web/live/member_live/form.ex:53
+#: lib/mv_web/live/member_live/show.ex:58
#, elixir-autogen, elixir-format
msgid "Notes"
msgstr ""
-#: lib/mv_web/live/components/payment_filter_component.ex:94
-#: lib/mv_web/live/components/payment_filter_component.ex:144
#: lib/mv_web/live/contribution_period_live/show.ex:186
#: lib/mv_web/live/contribution_period_live/show.ex:243
-#: lib/mv_web/live/member_live/form.ex:48
-#: lib/mv_web/live/member_live/index.html.heex:224
-#: lib/mv_web/live/member_live/show.ex:51
+#: lib/mv_web/live/member_live/form.ex:49
+#: lib/mv_web/live/member_live/show.ex:52
#, elixir-autogen, elixir-format
msgid "Paid"
msgstr ""
-#: lib/mv_web/live/member_live/form.ex:49
-#: lib/mv_web/live/member_live/index.html.heex:197
-#: lib/mv_web/live/member_live/show.ex:54
+#: 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/show.ex:55
#, elixir-autogen, elixir-format
msgid "Phone Number"
msgstr ""
-#: lib/mv_web/live/member_live/form.ex:56
-#: lib/mv_web/live/member_live/index.html.heex:161
-#: lib/mv_web/live/member_live/show.ex:61
+#: 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/show.ex:62
#, elixir-autogen, elixir-format
msgid "Postal Code"
msgstr ""
-#: lib/mv_web/live/member_live/form.ex:79
+#: lib/mv_web/live/member_live/form.ex:80
#, elixir-autogen, elixir-format
msgid "Save Member"
msgstr ""
@@ -166,15 +169,15 @@ msgstr ""
#: lib/mv_web/live/custom_field_live/form.ex:66
#: lib/mv_web/live/custom_field_value_live/form.ex:74
#: lib/mv_web/live/global_settings_live.ex:55
-#: lib/mv_web/live/member_live/form.ex:78
+#: lib/mv_web/live/member_live/form.ex:79
#: lib/mv_web/live/user_live/form.ex:248
#, elixir-autogen, elixir-format
msgid "Saving..."
msgstr ""
-#: lib/mv_web/live/member_live/form.ex:54
-#: lib/mv_web/live/member_live/index.html.heex:125
-#: lib/mv_web/live/member_live/show.ex:59
+#: 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/show.ex:60
#, elixir-autogen, elixir-format
msgid "Street"
msgstr ""
@@ -184,14 +187,13 @@ msgstr ""
msgid "Id"
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/show.ex:52
+#: lib/mv_web/live/member_live/show.ex:53
#, elixir-autogen, elixir-format
msgid "No"
msgstr ""
-#: lib/mv_web/live/member_live/show.ex:115
+#: lib/mv_web/live/member_live/show.ex:116
#, elixir-autogen, elixir-format
msgid "Show Member"
msgstr ""
@@ -201,23 +203,22 @@ msgstr ""
msgid "This is a member record from your database."
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/show.ex:52
+#: lib/mv_web/live/member_live/show.ex:53
#, elixir-autogen, elixir-format
msgid "Yes"
msgstr ""
#: lib/mv_web/live/custom_field_live/form.ex:110
#: lib/mv_web/live/custom_field_value_live/form.ex:233
-#: lib/mv_web/live/member_live/form.ex:137
+#: lib/mv_web/live/member_live/form.ex:138
#, elixir-autogen, elixir-format
msgid "create"
msgstr ""
#: lib/mv_web/live/custom_field_live/form.ex:111
#: lib/mv_web/live/custom_field_value_live/form.ex:234
-#: lib/mv_web/live/member_live/form.ex:138
+#: lib/mv_web/live/member_live/form.ex:139
#, elixir-autogen, elixir-format
msgid "update"
msgstr ""
@@ -227,7 +228,7 @@ msgstr ""
msgid "Incorrect email or password"
msgstr ""
-#: lib/mv_web/live/member_live/form.ex:144
+#: lib/mv_web/live/member_live/form.ex:145
#, elixir-autogen, elixir-format
msgid "Member %{action} successfully"
msgstr ""
@@ -260,7 +261,7 @@ msgstr ""
#: lib/mv_web/live/custom_field_live/form.ex:69
#: lib/mv_web/live/custom_field_live/index.ex:120
#: lib/mv_web/live/custom_field_value_live/form.ex:77
-#: lib/mv_web/live/member_live/form.ex:81
+#: lib/mv_web/live/member_live/form.ex:82
#: lib/mv_web/live/user_live/form.ex:251
#, elixir-autogen, elixir-format
msgid "Cancel"
@@ -371,12 +372,12 @@ msgstr ""
msgid "Required"
msgstr ""
-#: lib/mv_web/live/member_live/index.html.heex:63
+#: lib/mv_web/live/member_live/index.html.heex:55
#, elixir-autogen, elixir-format
msgid "Select all members"
msgstr ""
-#: lib/mv_web/live/member_live/index.html.heex:77
+#: lib/mv_web/live/member_live/index.html.heex:69
#, elixir-autogen, elixir-format
msgid "Select member"
msgstr ""
@@ -521,7 +522,7 @@ msgstr ""
msgid "Linked Member"
msgstr ""
-#: lib/mv_web/live/member_live/show.ex:62
+#: lib/mv_web/live/member_live/show.ex:63
#, elixir-autogen, elixir-format
msgid "Linked User"
msgstr ""
@@ -532,7 +533,7 @@ msgstr ""
msgid "No member linked"
msgstr ""
-#: lib/mv_web/live/member_live/show.ex:72
+#: lib/mv_web/live/member_live/show.ex:73
#, elixir-autogen, elixir-format
msgid "No user linked"
msgstr ""
@@ -562,7 +563,7 @@ msgid "Toggle dark mode"
msgstr ""
#: lib/mv_web/live/components/search_bar_component.ex:15
-#: lib/mv_web/live/member_live/index.html.heex:34
+#: lib/mv_web/live/member_live/index.html.heex:33
#, elixir-autogen, elixir-format
msgid "Search..."
msgstr ""
@@ -578,7 +579,7 @@ msgstr ""
msgid "Click to sort"
msgstr ""
-#: lib/mv_web/live/member_live/index.html.heex:89
+#: lib/mv_web/live/member_live/index.html.heex:81
#, elixir-autogen, elixir-format
msgid "First name"
msgstr ""
@@ -619,8 +620,8 @@ msgstr ""
msgid "Choose a custom field"
msgstr ""
-#: lib/mv_web/live/member_live/form.ex:58
-#: lib/mv_web/live/member_live/show.ex:77
+#: lib/mv_web/live/member_live/form.ex:59
+#: lib/mv_web/live/member_live/show.ex:78
#, elixir-autogen, elixir-format
msgid "Custom Field Values"
msgstr ""
@@ -790,7 +791,7 @@ msgstr ""
msgid "Unlinking scheduled"
msgstr ""
-#: lib/mv_web/live/member_live/index.ex:165
+#: lib/mv_web/live/member_live/index.ex:164
#, elixir-autogen, elixir-format
msgid "Copied %{count} email address to clipboard"
msgid_plural "Copied %{count} email addresses to clipboard"
@@ -807,12 +808,12 @@ msgstr ""
msgid "Copy emails"
msgstr ""
-#: lib/mv_web/live/member_live/index.ex:154
+#: lib/mv_web/live/member_live/index.ex:153
#, elixir-autogen, elixir-format
msgid "No email addresses found"
msgstr ""
-#: lib/mv_web/live/member_live/index.ex:151
+#: lib/mv_web/live/member_live/index.ex:150
#, elixir-autogen, elixir-format
msgid "No members selected"
msgstr ""
@@ -827,7 +828,7 @@ msgstr ""
msgid "Open in email program"
msgstr ""
-#: lib/mv_web/live/member_live/index.ex:174
+#: lib/mv_web/live/member_live/index.ex:173
#, elixir-autogen, elixir-format
msgid "Tip: Paste email addresses into the BCC field for privacy compliance"
msgstr ""
@@ -845,28 +846,6 @@ msgstr ""
msgid "This field cannot be empty"
msgstr ""
-#: lib/mv_web/live/components/payment_filter_component.ex:80
-#: lib/mv_web/live/components/payment_filter_component.ex:143
-#, elixir-autogen, elixir-format
-msgid "All"
-msgstr ""
-
-#: lib/mv_web/live/components/payment_filter_component.ex:54
-#, elixir-autogen, elixir-format
-msgid "Filter by payment status"
-msgstr ""
-
-#: lib/mv_web/live/components/payment_filter_component.ex:108
-#: lib/mv_web/live/components/payment_filter_component.ex:145
-#, elixir-autogen, elixir-format
-msgid "Not paid"
-msgstr ""
-
-#: lib/mv_web/live/components/payment_filter_component.ex:65
-#, elixir-autogen, elixir-format
-msgid "Payment filter"
-msgstr ""
-
#: lib/mv_web/live/contribution_type_live/index.ex:113
#, elixir-autogen, elixir-format
msgid "About Contribution Types"
diff --git a/priv/gettext/en/LC_MESSAGES/default.po b/priv/gettext/en/LC_MESSAGES/default.po
index 46b37bf..77db41c 100644
--- a/priv/gettext/en/LC_MESSAGES/default.po
+++ b/priv/gettext/en/LC_MESSAGES/default.po
@@ -17,7 +17,7 @@ msgstr ""
msgid "Actions"
msgstr ""
-#: lib/mv_web/live/member_live/index.html.heex:243
+#: lib/mv_web/live/member_live/index.html.heex:227
#: lib/mv_web/live/user_live/index.html.heex:72
#, elixir-autogen, elixir-format
msgid "Are you sure?"
@@ -29,22 +29,22 @@ msgstr ""
msgid "Attempting to reconnect"
msgstr ""
-#: lib/mv_web/live/member_live/form.ex:53
-#: lib/mv_web/live/member_live/index.html.heex:179
-#: lib/mv_web/live/member_live/show.ex:58
+#: 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/show.ex:59
#, elixir-autogen, elixir-format
msgid "City"
msgstr ""
#: lib/mv_web/live/contribution_type_live/index.ex:78
-#: lib/mv_web/live/member_live/index.html.heex:245
+#: lib/mv_web/live/member_live/index.html.heex:229
#: lib/mv_web/live/user_live/index.html.heex:74
#, elixir-autogen, elixir-format
msgid "Delete"
msgstr ""
#: lib/mv_web/live/contribution_type_live/index.ex:66
-#: lib/mv_web/live/member_live/index.html.heex:237
+#: lib/mv_web/live/member_live/index.html.heex:221
#: lib/mv_web/live/user_live/form.ex:265
#: lib/mv_web/live/user_live/index.html.heex:66
#, elixir-autogen, elixir-format
@@ -52,14 +52,14 @@ msgid "Edit"
msgstr ""
#: lib/mv_web/live/member_live/show.ex:41
-#: lib/mv_web/live/member_live/show.ex:116
+#: lib/mv_web/live/member_live/show.ex:117
#, elixir-autogen, elixir-format
msgid "Edit Member"
msgstr ""
#: lib/mv_web/live/contribution_period_live/show.ex:58
#: lib/mv_web/live/member_live/form.ex:47
-#: lib/mv_web/live/member_live/index.html.heex:107
+#: lib/mv_web/live/member_live/index.html.heex:99
#: lib/mv_web/live/member_live/show.ex:50
#: lib/mv_web/live/user_live/form.ex:46
#: lib/mv_web/live/user_live/index.html.heex:44
@@ -74,9 +74,9 @@ msgstr ""
msgid "First Name"
msgstr ""
-#: lib/mv_web/live/member_live/form.ex:50
-#: lib/mv_web/live/member_live/index.html.heex:215
-#: lib/mv_web/live/member_live/show.ex:55
+#: 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/show.ex:56
#, elixir-autogen, elixir-format
msgid "Join Date"
msgstr ""
@@ -92,7 +92,7 @@ msgstr ""
msgid "New Member"
msgstr ""
-#: lib/mv_web/live/member_live/index.html.heex:234
+#: lib/mv_web/live/member_live/index.html.heex:218
#: lib/mv_web/live/user_live/index.html.heex:63
#, elixir-autogen, elixir-format
msgid "Show"
@@ -113,52 +113,55 @@ msgstr ""
msgid "close"
msgstr ""
-#: lib/mv_web/live/member_live/form.ex:51
-#: lib/mv_web/live/member_live/show.ex:56
+#: lib/mv_web/live/member_live/form.ex:48
+#: lib/mv_web/live/member_live/show.ex:51
+#, elixir-autogen, elixir-format
+msgid "Birth Date"
+msgstr ""
+
+#: lib/mv_web/live/member_live/form.ex:52
+#: lib/mv_web/live/member_live/show.ex:57
#, elixir-autogen, elixir-format
msgid "Exit Date"
msgstr ""
-#: lib/mv_web/live/member_live/form.ex:55
-#: lib/mv_web/live/member_live/index.html.heex:143
-#: lib/mv_web/live/member_live/show.ex:60
+#: 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/show.ex:61
#, elixir-autogen, elixir-format
msgid "House Number"
msgstr ""
#: lib/mv_web/live/contribution_period_live/show.ex:140
-#: lib/mv_web/live/member_live/form.ex:52
-#: lib/mv_web/live/member_live/show.ex:57
+#: lib/mv_web/live/member_live/form.ex:53
+#: lib/mv_web/live/member_live/show.ex:58
#, elixir-autogen, elixir-format
msgid "Notes"
msgstr ""
-#: lib/mv_web/live/components/payment_filter_component.ex:94
-#: lib/mv_web/live/components/payment_filter_component.ex:144
#: lib/mv_web/live/contribution_period_live/show.ex:186
#: lib/mv_web/live/contribution_period_live/show.ex:243
-#: lib/mv_web/live/member_live/form.ex:48
-#: lib/mv_web/live/member_live/index.html.heex:224
-#: lib/mv_web/live/member_live/show.ex:51
+#: lib/mv_web/live/member_live/form.ex:49
+#: lib/mv_web/live/member_live/show.ex:52
#, elixir-autogen, elixir-format
msgid "Paid"
msgstr ""
-#: lib/mv_web/live/member_live/form.ex:49
-#: lib/mv_web/live/member_live/index.html.heex:197
-#: lib/mv_web/live/member_live/show.ex:54
+#: 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/show.ex:55
#, elixir-autogen, elixir-format
msgid "Phone Number"
msgstr ""
-#: lib/mv_web/live/member_live/form.ex:56
-#: lib/mv_web/live/member_live/index.html.heex:161
-#: lib/mv_web/live/member_live/show.ex:61
+#: 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/show.ex:62
#, elixir-autogen, elixir-format
msgid "Postal Code"
msgstr ""
-#: lib/mv_web/live/member_live/form.ex:79
+#: lib/mv_web/live/member_live/form.ex:80
#, elixir-autogen, elixir-format, fuzzy
msgid "Save Member"
msgstr ""
@@ -166,15 +169,15 @@ msgstr ""
#: lib/mv_web/live/custom_field_live/form.ex:66
#: lib/mv_web/live/custom_field_value_live/form.ex:74
#: lib/mv_web/live/global_settings_live.ex:55
-#: lib/mv_web/live/member_live/form.ex:78
+#: lib/mv_web/live/member_live/form.ex:79
#: lib/mv_web/live/user_live/form.ex:248
#, elixir-autogen, elixir-format
msgid "Saving..."
msgstr ""
-#: lib/mv_web/live/member_live/form.ex:54
-#: lib/mv_web/live/member_live/index.html.heex:125
-#: lib/mv_web/live/member_live/show.ex:59
+#: 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/show.ex:60
#, elixir-autogen, elixir-format
msgid "Street"
msgstr ""
@@ -184,14 +187,13 @@ msgstr ""
msgid "Id"
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/show.ex:52
+#: lib/mv_web/live/member_live/show.ex:53
#, elixir-autogen, elixir-format
msgid "No"
msgstr ""
-#: lib/mv_web/live/member_live/show.ex:115
+#: lib/mv_web/live/member_live/show.ex:116
#, elixir-autogen, elixir-format, fuzzy
msgid "Show Member"
msgstr ""
@@ -201,23 +203,22 @@ msgstr ""
msgid "This is a member record from your database."
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/show.ex:52
+#: lib/mv_web/live/member_live/show.ex:53
#, elixir-autogen, elixir-format
msgid "Yes"
msgstr ""
#: lib/mv_web/live/custom_field_live/form.ex:110
#: lib/mv_web/live/custom_field_value_live/form.ex:233
-#: lib/mv_web/live/member_live/form.ex:137
+#: lib/mv_web/live/member_live/form.ex:138
#, elixir-autogen, elixir-format
msgid "create"
msgstr ""
#: lib/mv_web/live/custom_field_live/form.ex:111
#: lib/mv_web/live/custom_field_value_live/form.ex:234
-#: lib/mv_web/live/member_live/form.ex:138
+#: lib/mv_web/live/member_live/form.ex:139
#, elixir-autogen, elixir-format
msgid "update"
msgstr ""
@@ -227,7 +228,7 @@ msgstr ""
msgid "Incorrect email or password"
msgstr ""
-#: lib/mv_web/live/member_live/form.ex:144
+#: lib/mv_web/live/member_live/form.ex:145
#, elixir-autogen, elixir-format
msgid "Member %{action} successfully"
msgstr ""
@@ -260,7 +261,7 @@ msgstr ""
#: lib/mv_web/live/custom_field_live/form.ex:69
#: lib/mv_web/live/custom_field_live/index.ex:120
#: lib/mv_web/live/custom_field_value_live/form.ex:77
-#: lib/mv_web/live/member_live/form.ex:81
+#: lib/mv_web/live/member_live/form.ex:82
#: lib/mv_web/live/user_live/form.ex:251
#, elixir-autogen, elixir-format
msgid "Cancel"
@@ -371,12 +372,12 @@ msgstr ""
msgid "Required"
msgstr ""
-#: lib/mv_web/live/member_live/index.html.heex:63
+#: lib/mv_web/live/member_live/index.html.heex:55
#, elixir-autogen, elixir-format
msgid "Select all members"
msgstr ""
-#: lib/mv_web/live/member_live/index.html.heex:77
+#: lib/mv_web/live/member_live/index.html.heex:69
#, elixir-autogen, elixir-format
msgid "Select member"
msgstr ""
@@ -521,7 +522,7 @@ msgstr "User will be created without a password. Check 'Set Password' to add one
msgid "Linked Member"
msgstr ""
-#: lib/mv_web/live/member_live/show.ex:62
+#: lib/mv_web/live/member_live/show.ex:63
#, elixir-autogen, elixir-format
msgid "Linked User"
msgstr ""
@@ -532,7 +533,7 @@ msgstr ""
msgid "No member linked"
msgstr ""
-#: lib/mv_web/live/member_live/show.ex:72
+#: lib/mv_web/live/member_live/show.ex:73
#, elixir-autogen, elixir-format
msgid "No user linked"
msgstr ""
@@ -562,7 +563,7 @@ msgid "Toggle dark mode"
msgstr ""
#: lib/mv_web/live/components/search_bar_component.ex:15
-#: lib/mv_web/live/member_live/index.html.heex:34
+#: lib/mv_web/live/member_live/index.html.heex:33
#, elixir-autogen, elixir-format
msgid "Search..."
msgstr ""
@@ -578,7 +579,7 @@ msgstr ""
msgid "Click to sort"
msgstr ""
-#: lib/mv_web/live/member_live/index.html.heex:89
+#: lib/mv_web/live/member_live/index.html.heex:81
#, elixir-autogen, elixir-format, fuzzy
msgid "First name"
msgstr ""
@@ -619,8 +620,8 @@ msgstr ""
msgid "Choose a custom field"
msgstr ""
-#: lib/mv_web/live/member_live/form.ex:58
-#: lib/mv_web/live/member_live/show.ex:77
+#: lib/mv_web/live/member_live/form.ex:59
+#: lib/mv_web/live/member_live/show.ex:78
#, elixir-autogen, elixir-format
msgid "Custom Field Values"
msgstr ""
@@ -790,7 +791,7 @@ msgstr ""
msgid "Unlinking scheduled"
msgstr ""
-#: lib/mv_web/live/member_live/index.ex:165
+#: lib/mv_web/live/member_live/index.ex:164
#, elixir-autogen, elixir-format
msgid "Copied %{count} email address to clipboard"
msgid_plural "Copied %{count} email addresses to clipboard"
@@ -807,12 +808,12 @@ msgstr ""
msgid "Copy emails"
msgstr ""
-#: lib/mv_web/live/member_live/index.ex:154
+#: lib/mv_web/live/member_live/index.ex:153
#, elixir-autogen, elixir-format
msgid "No email addresses found"
msgstr ""
-#: lib/mv_web/live/member_live/index.ex:151
+#: lib/mv_web/live/member_live/index.ex:150
#, elixir-autogen, elixir-format, fuzzy
msgid "No members selected"
msgstr ""
@@ -827,7 +828,7 @@ msgstr ""
msgid "Open in email program"
msgstr ""
-#: lib/mv_web/live/member_live/index.ex:174
+#: lib/mv_web/live/member_live/index.ex:173
#, elixir-autogen, elixir-format
msgid "Tip: Paste email addresses into the BCC field for privacy compliance"
msgstr ""
@@ -845,28 +846,6 @@ msgstr ""
msgid "This field cannot be empty"
msgstr ""
-#: lib/mv_web/live/components/payment_filter_component.ex:80
-#: lib/mv_web/live/components/payment_filter_component.ex:143
-#, elixir-autogen, elixir-format
-msgid "All"
-msgstr ""
-
-#: lib/mv_web/live/components/payment_filter_component.ex:54
-#, elixir-autogen, elixir-format
-msgid "Filter by payment status"
-msgstr ""
-
-#: lib/mv_web/live/components/payment_filter_component.ex:108
-#: lib/mv_web/live/components/payment_filter_component.ex:145
-#, elixir-autogen, elixir-format
-msgid "Not paid"
-msgstr ""
-
-#: lib/mv_web/live/components/payment_filter_component.ex:65
-#, elixir-autogen, elixir-format
-msgid "Payment filter"
-msgstr ""
-
#: lib/mv_web/live/contribution_type_live/index.ex:113
#, elixir-autogen, elixir-format
msgid "About Contribution Types"
diff --git a/priv/repo/migrations/20251202145404_remove_birth_date_from_members.exs b/priv/repo/migrations/20251202145404_remove_birth_date_from_members.exs
deleted file mode 100644
index 4a6cf3a..0000000
--- a/priv/repo/migrations/20251202145404_remove_birth_date_from_members.exs
+++ /dev/null
@@ -1,69 +0,0 @@
-defmodule Mv.Repo.Migrations.RemoveBirthDateFromMembers do
- @moduledoc """
- Removes the birth_date column from the members table.
-
- The birth_date field has been removed from the application because most users
- don't record birthday data. Users who need this can use a custom field instead.
-
- This migration also updates the search_vector trigger to remove birth_date.
- """
-
- use Ecto.Migration
-
- def up do
- # Update the trigger function to remove birth_date from search_vector
- execute("""
- CREATE OR REPLACE FUNCTION members_search_vector_trigger() RETURNS trigger AS $$
- BEGIN
- NEW.search_vector :=
- setweight(to_tsvector('simple', coalesce(NEW.first_name, '')), 'A') ||
- setweight(to_tsvector('simple', coalesce(NEW.last_name, '')), 'A') ||
- setweight(to_tsvector('simple', coalesce(NEW.email, '')), 'B') ||
- setweight(to_tsvector('simple', coalesce(NEW.phone_number, '')), 'C') ||
- setweight(to_tsvector('simple', coalesce(NEW.join_date::text, '')), 'D') ||
- setweight(to_tsvector('simple', coalesce(NEW.exit_date::text, '')), 'D') ||
- setweight(to_tsvector('simple', coalesce(NEW.notes, '')), 'B') ||
- setweight(to_tsvector('simple', coalesce(NEW.city, '')), 'C') ||
- setweight(to_tsvector('simple', coalesce(NEW.street, '')), 'C') ||
- setweight(to_tsvector('simple', coalesce(NEW.house_number::text, '')), 'C') ||
- setweight(to_tsvector('simple', coalesce(NEW.postal_code::text, '')), 'C');
- RETURN NEW;
- END
- $$ LANGUAGE plpgsql;
- """)
-
- # Remove the birth_date column
- alter table(:members) do
- remove :birth_date
- end
- end
-
- def down do
- # Add the birth_date column back
- alter table(:members) do
- add :birth_date, :date
- end
-
- # Restore the trigger function with birth_date
- execute("""
- CREATE OR REPLACE FUNCTION members_search_vector_trigger() RETURNS trigger AS $$
- BEGIN
- NEW.search_vector :=
- setweight(to_tsvector('simple', coalesce(NEW.first_name, '')), 'A') ||
- setweight(to_tsvector('simple', coalesce(NEW.last_name, '')), 'A') ||
- setweight(to_tsvector('simple', coalesce(NEW.email, '')), 'B') ||
- setweight(to_tsvector('simple', coalesce(NEW.birth_date::text, '')), 'C') ||
- setweight(to_tsvector('simple', coalesce(NEW.phone_number, '')), 'C') ||
- setweight(to_tsvector('simple', coalesce(NEW.join_date::text, '')), 'D') ||
- setweight(to_tsvector('simple', coalesce(NEW.exit_date::text, '')), 'D') ||
- setweight(to_tsvector('simple', coalesce(NEW.notes, '')), 'B') ||
- setweight(to_tsvector('simple', coalesce(NEW.city, '')), 'C') ||
- setweight(to_tsvector('simple', coalesce(NEW.street, '')), 'C') ||
- setweight(to_tsvector('simple', coalesce(NEW.house_number::text, '')), 'C') ||
- setweight(to_tsvector('simple', coalesce(NEW.postal_code::text, '')), 'C');
- RETURN NEW;
- END
- $$ LANGUAGE plpgsql;
- """)
- end
-end
diff --git a/priv/repo/seeds.exs b/priv/repo/seeds.exs
index bec9006..542e559 100644
--- a/priv/repo/seeds.exs
+++ b/priv/repo/seeds.exs
@@ -112,6 +112,7 @@ for member_attrs <- [
first_name: "Hans",
last_name: "Müller",
email: "hans.mueller@example.de",
+ birth_date: ~D[1985-06-15],
join_date: ~D[2023-01-15],
paid: true,
phone_number: "+49301234567",
@@ -124,6 +125,7 @@ for member_attrs <- [
first_name: "Greta",
last_name: "Schmidt",
email: "greta.schmidt@example.de",
+ birth_date: ~D[1990-03-22],
join_date: ~D[2023-02-01],
paid: false,
phone_number: "+49309876543",
@@ -137,6 +139,7 @@ for member_attrs <- [
first_name: "Friedrich",
last_name: "Wagner",
email: "friedrich.wagner@example.de",
+ birth_date: ~D[1978-11-08],
join_date: ~D[2022-11-10],
paid: true,
phone_number: "+49301122334",
@@ -148,6 +151,7 @@ for member_attrs <- [
first_name: "Marianne",
last_name: "Wagner",
email: "marianne.wagner@example.de",
+ birth_date: ~D[1978-11-08],
join_date: ~D[2022-11-10],
paid: true,
phone_number: "+49301122334",
@@ -182,6 +186,7 @@ linked_members = [
first_name: "Maria",
last_name: "Weber",
email: "maria.weber@example.de",
+ birth_date: ~D[1992-07-14],
join_date: ~D[2023-03-15],
paid: true,
phone_number: "+49301357924",
@@ -197,6 +202,7 @@ linked_members = [
first_name: "Thomas",
last_name: "Klein",
email: "thomas.klein@example.de",
+ birth_date: ~D[1988-12-03],
join_date: ~D[2023-04-01],
paid: false,
phone_number: "+49302468135",
diff --git a/test/membership/member_test.exs b/test/membership/member_test.exs
index 1bf594a..7015d34 100644
--- a/test/membership/member_test.exs
+++ b/test/membership/member_test.exs
@@ -6,6 +6,7 @@ defmodule Mv.Membership.MemberTest do
@valid_attrs %{
first_name: "John",
last_name: "Doe",
+ birth_date: ~D[1990-01-01],
paid: true,
email: "john@example.com",
phone_number: "+49123456789",
@@ -42,6 +43,12 @@ defmodule Mv.Membership.MemberTest do
assert error_message(errors, :email) =~ "is not a valid email"
end
+ test "Birth date is optional but must not be in the future" do
+ attrs = Map.put(@valid_attrs, :birth_date, Date.utc_today() |> Date.add(1))
+ assert {:error, %Ash.Error.Invalid{errors: errors}} = Membership.create_member(attrs)
+ assert error_message(errors, :birth_date) =~ "cannot be in the future"
+ end
+
test "Paid is optional but must be boolean if specified" do
attrs = Map.put(@valid_attrs, :paid, nil)
attrs2 = Map.put(@valid_attrs, :paid, "yes")
diff --git a/test/mv_web/components/payment_filter_component_test.exs b/test/mv_web/components/payment_filter_component_test.exs
deleted file mode 100644
index c44bf41..0000000
--- a/test/mv_web/components/payment_filter_component_test.exs
+++ /dev/null
@@ -1,183 +0,0 @@
-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
- """
- # async: false to prevent PostgreSQL deadlocks when running LiveView tests against DB
- use MvWeb.ConnCase, async: false
-
- 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
diff --git a/test/mv_web/member_live/index_custom_fields_display_test.exs b/test/mv_web/member_live/index_custom_fields_display_test.exs
index 0485f5e..25aefe5 100644
--- a/test/mv_web/member_live/index_custom_fields_display_test.exs
+++ b/test/mv_web/member_live/index_custom_fields_display_test.exs
@@ -9,8 +9,7 @@ defmodule MvWeb.MemberLive.IndexCustomFieldsDisplayTest do
- Custom field values are correctly formatted for different types
- Members without custom field values show empty cell or "-"
"""
- # async: false to prevent PostgreSQL deadlocks when creating members and custom fields
- use MvWeb.ConnCase, async: false
+ use MvWeb.ConnCase, async: true
import Phoenix.LiveViewTest
require Ash.Query
diff --git a/test/mv_web/member_live/index_test.exs b/test/mv_web/member_live/index_test.exs
index 0bcc731..e3ad5bb 100644
--- a/test/mv_web/member_live/index_test.exs
+++ b/test/mv_web/member_live/index_test.exs
@@ -469,221 +469,4 @@ defmodule MvWeb.MemberLive.IndexTest do
assert has_element?(view, "#flash-group")
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