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