Compare commits

..

10 commits

Author SHA1 Message Date
49184d2631
feat: Add contribution management mock-up pages
All checks were successful
continuous-integration/drone/push Build is passing
Add non-functional preview pages for Contribution Types, Settings, and Member Contribution Periods with German translations
2025-12-02 16:44:49 +01:00
75fe26fad8
resolve review issues 2025-12-02 16:40:51 +01:00
fcf7cbc558
docs: payment concept 2025-12-02 16:40:51 +01:00
ac2ad0a0d5 Merge pull request 'Implement filter for has_paid closes #227' (#237) from feature/227_payment_filter into main
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #237
2025-12-02 16:12:42 +01:00
875c422b7d
Fix missing search query socket assign in member index
All checks were successful
continuous-integration/drone/push Build is passing
2025-12-02 16:04:07 +01:00
6d75766dba
fix: add ESC key support, security comment, and disable async tests
All checks were successful
continuous-integration/drone/push Build is passing
2025-12-02 15:55:27 +01:00
354029c9cc
fix: add role=none to li elements in payment filter for ARIA compliance 2025-12-02 15:55:26 +01:00
671e6ce804
feat: add payment status filter and paid column to member list
Add PaymentFilterComponent dropdown and colored paid column. Filter supports URL bookmarking and combines with search/sort.
2025-12-02 15:55:23 +01:00
386b4c9e65 Merge pull request 'Don't show birthday field for default configurations closes #161' (#239) from feature/161_remove_birthday into main
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #239
Reviewed-by: rafael <rafael@noreply.git.local-it.org>
2025-12-02 15:48:59 +01:00
c8968636a8 feat: remove birth_date field from Member resource
All checks were successful
continuous-integration/drone/push Build is passing
Users who need birthday data can use custom fields instead.
Closes #161
2025-12-02 14:58:50 +01:00
19 changed files with 1005 additions and 230 deletions

View file

@ -115,7 +115,6 @@ 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}`
@ -169,7 +168,7 @@ Member (1) → (N) Properties
### Weighted Fields
- **Weight A (highest):** first_name, last_name
- **Weight B:** email, notes
- **Weight C:** birth_date, phone_number, city, street, house_number, postal_code
- **Weight C:** phone_number, city, street, house_number, postal_code
- **Weight D (lowest):** join_date, exit_date
### Usage Example
@ -381,7 +380,7 @@ Install "DBML Language" extension to view/edit DBML files with:
- tokens (jti, purpose, extra_data)
**Personal Data (GDPR):**
- All member fields (name, email, birth_date, address)
- All member fields (name, email, address)
- User email
- Token subject

View file

@ -122,7 +122,6 @@ 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)']
@ -153,7 +152,7 @@ Table members {
**Club Member Master Data**
Core entity for membership management containing:
- Personal information (name, birth date, email)
- Personal information (name, email)
- Contact details (phone, address)
- Membership status (join/exit dates, payment status)
- Additional notes
@ -183,7 +182,6 @@ 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}$

View file

@ -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:**

View file

@ -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: birth_date and join_date not in future, exit_date after join_date
- Date validations: join_date not in future, exit_date after join_date
- Email uniqueness: prevents conflicts with unlinked users
## Full-Text Search
@ -284,11 +284,6 @@ 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)],
@ -351,10 +346,6 @@ 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

View file

@ -7,7 +7,6 @@ defmodule Mv.Constants do
:first_name,
:last_name,
:email,
:birth_date,
:paid,
:phone_number,
:join_date,

View file

@ -0,0 +1,146 @@
defmodule MvWeb.Components.PaymentFilterComponent do
@moduledoc """
Provides the PaymentFilter Live-Component.
A dropdown filter for filtering members by payment status (paid/not paid/all).
Uses DaisyUI dropdown styling and sends filter changes to parent LiveView.
## Props
- `:paid_filter` - Current filter state: `nil` (all), `:paid`, or `:not_paid`
- `:id` - Component ID (required)
- `:member_count` - Number of filtered members to display in badge (optional, default: 0)
## Events
- Sends `{:payment_filter_changed, filter}` to parent when filter changes
"""
use MvWeb, :live_component
@impl true
def mount(socket) do
{:ok, assign(socket, :open, false)}
end
@impl true
def update(assigns, socket) do
socket =
socket
|> assign(:id, assigns.id)
|> assign(:paid_filter, assigns[:paid_filter])
|> assign(:member_count, assigns[:member_count] || 0)
{:ok, socket}
end
@impl true
def render(assigns) do
~H"""
<div
class="relative"
id={@id}
phx-window-keydown={@open && "close_dropdown"}
phx-key="Escape"
phx-target={@myself}
>
<button
type="button"
class={[
"btn btn-ghost gap-2",
@paid_filter && "btn-active"
]}
phx-click="toggle_dropdown"
phx-target={@myself}
aria-haspopup="true"
aria-expanded={to_string(@open)}
aria-label={gettext("Filter by payment status")}
>
<.icon name="hero-funnel" class="h-5 w-5" />
<span class="hidden sm:inline">{filter_label(@paid_filter)}</span>
<span :if={@paid_filter} class="badge badge-primary badge-sm">{@member_count}</span>
</button>
<ul
:if={@open}
class="menu dropdown-content bg-base-100 rounded-box z-10 w-52 p-2 shadow-lg absolute right-0 mt-2"
role="menu"
aria-label={gettext("Payment filter")}
phx-click-away="close_dropdown"
phx-target={@myself}
>
<li role="none">
<button
type="button"
role="menuitemradio"
aria-checked={to_string(@paid_filter == nil)}
class={@paid_filter == nil && "active"}
phx-click="select_filter"
phx-value-filter=""
phx-target={@myself}
>
<.icon name="hero-users" class="h-4 w-4" />
{gettext("All")}
</button>
</li>
<li role="none">
<button
type="button"
role="menuitemradio"
aria-checked={to_string(@paid_filter == :paid)}
class={@paid_filter == :paid && "active"}
phx-click="select_filter"
phx-value-filter="paid"
phx-target={@myself}
>
<.icon name="hero-check-circle" class="h-4 w-4 text-success" />
{gettext("Paid")}
</button>
</li>
<li role="none">
<button
type="button"
role="menuitemradio"
aria-checked={to_string(@paid_filter == :not_paid)}
class={@paid_filter == :not_paid && "active"}
phx-click="select_filter"
phx-value-filter="not_paid"
phx-target={@myself}
>
<.icon name="hero-x-circle" class="h-4 w-4 text-error" />
{gettext("Not paid")}
</button>
</li>
</ul>
</div>
"""
end
@impl true
def handle_event("toggle_dropdown", _params, socket) do
{:noreply, assign(socket, :open, !socket.assigns.open)}
end
@impl true
def handle_event("close_dropdown", _params, socket) do
{:noreply, assign(socket, :open, false)}
end
@impl true
def handle_event("select_filter", %{"filter" => filter_str}, socket) do
filter = parse_filter(filter_str)
# Close dropdown and notify parent
socket = assign(socket, :open, false)
send(self(), {:payment_filter_changed, filter})
{:noreply, socket}
end
# Parse filter string to atom
defp parse_filter("paid"), do: :paid
defp parse_filter("not_paid"), do: :not_paid
defp parse_filter(_), do: nil
# Get display label for current filter
defp filter_label(nil), do: gettext("All")
defp filter_label(:paid), do: gettext("Paid")
defp filter_label(:not_paid), do: gettext("Not paid")
end

View file

@ -14,7 +14,7 @@ defmodule MvWeb.MemberLive.Form do
- first_name, last_name, email
**Optional:**
- birth_date, phone_number, address fields (city, street, house_number, postal_code)
- phone_number, address fields (city, street, house_number, postal_code)
- join_date, exit_date
- paid status
- notes
@ -45,7 +45,6 @@ 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" />

View file

@ -46,7 +46,7 @@ defmodule MvWeb.MemberLive.Index do
Initializes the LiveView state.
Sets up initial assigns for page title, search query, sort configuration,
and member selection. Actual data loading happens in `handle_params/3`.
payment filter, and member selection. Actual data loading happens in `handle_params/3`.
"""
@impl true
def mount(_params, _session, socket) do
@ -74,6 +74,7 @@ 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))
@ -207,17 +208,17 @@ defmodule MvWeb.MemberLive.Index do
@impl true
def handle_info({:search_changed, q}, socket) do
socket = load_members(socket, q)
socket =
socket
|> assign(:query, q)
|> load_members()
existing_field_query = socket.assigns.sort_field
existing_sort_query = socket.assigns.sort_order
# Build the URL with queries
query_params = %{
"query" => q,
"sort_field" => existing_field_query,
"sort_order" => existing_sort_query
}
query_params =
build_query_params(q, existing_field_query, existing_sort_query, socket.assigns.paid_filter)
# Set the new path with params
new_path = ~p"/members?#{query_params}"
@ -230,13 +231,38 @@ 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, and sort order,
Parses query parameters for search query, sort field, sort order, and payment filter,
then loads members accordingly. This enables bookmarkable URLs and
browser back/forward navigation.
"""
@ -246,7 +272,9 @@ defmodule MvWeb.MemberLive.Index do
socket
|> maybe_update_search(params)
|> maybe_update_sort(params)
|> load_members(params["query"])
|> maybe_update_paid_filter(params)
|> assign(:query, params["query"])
|> load_members()
|> prepare_dynamic_cols()
{:noreply, socket}
@ -337,11 +365,13 @@ defmodule MvWeb.MemberLive.Index do
field
end
query_params = %{
"query" => socket.assigns.query,
"sort_field" => field_str,
"sort_order" => Atom.to_string(order)
}
query_params =
build_query_params(
socket.assigns.query,
field_str,
Atom.to_string(order),
socket.assigns.paid_filter
)
new_path = ~p"/members?#{query_params}"
@ -352,13 +382,45 @@ defmodule MvWeb.MemberLive.Index do
)}
end
# Loads members from the database with custom field values and applies search/sort filters.
# Builds URL query parameters map including all filter/sort state.
# Converts paid_filter atom to string for URL.
defp build_query_params(query, sort_field, sort_order, paid_filter) do
field_str =
if is_atom(sort_field) do
Atom.to_string(sort_field)
else
sort_field
end
order_str =
if is_atom(sort_order) do
Atom.to_string(sort_order)
else
sort_order
end
base_params = %{
"query" => query,
"sort_field" => field_str,
"sort_order" => order_str
}
# Only add paid_filter to URL if it's set
case paid_filter do
nil -> base_params
:paid -> Map.put(base_params, "paid_filter", "paid")
:not_paid -> Map.put(base_params, "paid_filter", "not_paid")
end
end
# Loads members from the database with custom field values and applies search/sort/payment filters.
#
# Process:
# 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 sorting (database-level for regular fields, in-memory for custom fields)
# 4. Applies payment status filter if set
# 5. Applies sorting (database-level for regular fields, in-memory for custom fields)
#
# Performance Considerations:
# - Database-level filtering: Custom field values are filtered directly in the database
@ -370,7 +432,9 @@ defmodule MvWeb.MemberLive.Index do
# consider implementing pagination (see Issue #165).
#
# Returns the socket with `:members` assigned.
defp load_members(socket, search_query) do
defp load_members(socket) do
search_query = socket.assigns.query
query =
Mv.Membership.Member
|> Ash.Query.new()
@ -383,6 +447,9 @@ 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} =
@ -457,6 +524,24 @@ 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
@ -747,6 +832,29 @@ 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
# -------------------------------------------------------------

View file

@ -26,12 +26,20 @@
</:actions>
</.header>
<.live_component
module={MvWeb.Components.SearchBarComponent}
id="search-bar"
query={@query}
placeholder={gettext("Search...")}
/>
<div class="flex flex-wrap gap-4 items-center">
<.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)}
/>
</div>
<.table
id="members"
@ -213,6 +221,14 @@
>
{member.join_date}
</:col>
<:col :let={member} label={gettext("Paid")}>
<span class={[
"badge",
if(member.paid == true, do: "badge-success", else: "badge-error")
]}>
{if member.paid == true, do: gettext("Yes"), else: gettext("No")}
</span>
</:col>
<:action :let={member}>
<div class="sr-only">
<.link navigate={~p"/members/#{member}"}>{gettext("Show")}</.link>

View file

@ -10,7 +10,7 @@ defmodule MvWeb.MemberLive.Show do
- Return to member list
## Displayed Information
- Basic: name, email, dates (birth, join, exit)
- Basic: name, email, dates (join, exit)
- Contact: phone number
- Address: street, house number, postal code, city
- Status: paid flag
@ -48,7 +48,6 @@ defmodule MvWeb.MemberLive.Show do
<:item title={gettext("First Name")}>{@member.first_name}</:item>
<:item title={gettext("Last Name")}>{@member.last_name}</:item>
<:item title={gettext("Email")}>{@member.email}</:item>
<:item title={gettext("Birth Date")}>{@member.birth_date}</:item>
<:item title={gettext("Paid")}>
{if @member.paid, do: gettext("Yes"), else: gettext("No")}
</:item>

View file

@ -16,7 +16,7 @@ msgstr ""
msgid "Actions"
msgstr "Aktionen"
#: lib/mv_web/live/member_live/index.html.heex:227
#: lib/mv_web/live/member_live/index.html.heex:243
#: lib/mv_web/live/user_live/index.html.heex:72
#, 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:54
#: lib/mv_web/live/member_live/index.html.heex:171
#: lib/mv_web/live/member_live/show.ex:59
#: 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
#, 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:229
#: lib/mv_web/live/member_live/index.html.heex:245
#: 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:221
#: lib/mv_web/live/member_live/index.html.heex:237
#: lib/mv_web/live/user_live/form.ex:265
#: lib/mv_web/live/user_live/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:117
#: lib/mv_web/live/member_live/show.ex:116
#, 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:99
#: lib/mv_web/live/member_live/index.html.heex:107
#: 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:51
#: lib/mv_web/live/member_live/index.html.heex:207
#: lib/mv_web/live/member_live/show.ex:56
#: 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
#, 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:218
#: lib/mv_web/live/member_live/index.html.heex:234
#: lib/mv_web/live/user_live/index.html.heex:63
#, elixir-autogen, elixir-format
msgid "Show"
@ -112,55 +112,52 @@ msgstr "Keine Internetverbindung gefunden"
msgid "close"
msgstr "schließen"
#: 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
#: lib/mv_web/live/member_live/form.ex:51
#: lib/mv_web/live/member_live/show.ex:56
#, elixir-autogen, elixir-format
msgid "Exit Date"
msgstr "Austrittsdatum"
#: 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
#: 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
#, 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:53
#: lib/mv_web/live/member_live/show.ex:58
#: lib/mv_web/live/member_live/form.ex:52
#: lib/mv_web/live/member_live/show.ex:57
#, 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:49
#: lib/mv_web/live/member_live/show.ex:52
#: 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
#, elixir-autogen, elixir-format
msgid "Paid"
msgstr "Bezahlt"
#: 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
#: 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
#, elixir-autogen, elixir-format
msgid "Phone Number"
msgstr "Telefonnummer"
#: 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
#: 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
#, elixir-autogen, elixir-format
msgid "Postal Code"
msgstr "Postleitzahl"
#: lib/mv_web/live/member_live/form.ex:80
#: lib/mv_web/live/member_live/form.ex:79
#, elixir-autogen, elixir-format
msgid "Save Member"
msgstr "Mitglied speichern"
@ -168,15 +165,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:79
#: lib/mv_web/live/member_live/form.ex:78
#: 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:55
#: lib/mv_web/live/member_live/index.html.heex:117
#: lib/mv_web/live/member_live/show.ex:60
#: 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
#, elixir-autogen, elixir-format
msgid "Street"
msgstr "Straße"
@ -186,13 +183,14 @@ 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:53
#: lib/mv_web/live/member_live/show.ex:52
#, elixir-autogen, elixir-format
msgid "No"
msgstr "Nein"
#: lib/mv_web/live/member_live/show.ex:116
#: lib/mv_web/live/member_live/show.ex:115
#, elixir-autogen, elixir-format, fuzzy
msgid "Show Member"
msgstr "Mitglied anzeigen"
@ -202,22 +200,23 @@ 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:53
#: lib/mv_web/live/member_live/show.ex:52
#, 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:138
#: lib/mv_web/live/member_live/form.ex:137
#, 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:139
#: lib/mv_web/live/member_live/form.ex:138
#, elixir-autogen, elixir-format
msgid "update"
msgstr "aktualisiert"
@ -227,7 +226,7 @@ msgstr "aktualisiert"
msgid "Incorrect email or password"
msgstr "Falsche E-Mail oder Passwort"
#: lib/mv_web/live/member_live/form.ex:145
#: lib/mv_web/live/member_live/form.ex:144
#, elixir-autogen, elixir-format
msgid "Member %{action} successfully"
msgstr "Mitglied %{action} erfolgreich"
@ -260,7 +259,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:82
#: lib/mv_web/live/member_live/form.ex:81
#: lib/mv_web/live/user_live/form.ex:251
#, elixir-autogen, elixir-format
msgid "Cancel"
@ -371,12 +370,12 @@ msgstr "Profil"
msgid "Required"
msgstr "Erforderlich"
#: lib/mv_web/live/member_live/index.html.heex:55
#: lib/mv_web/live/member_live/index.html.heex:63
#, elixir-autogen, elixir-format
msgid "Select all members"
msgstr "Alle Mitglieder auswählen"
#: lib/mv_web/live/member_live/index.html.heex:69
#: lib/mv_web/live/member_live/index.html.heex:77
#, elixir-autogen, elixir-format
msgid "Select member"
msgstr "Mitglied auswählen"
@ -521,7 +520,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:63
#: lib/mv_web/live/member_live/show.ex:62
#, elixir-autogen, elixir-format
msgid "Linked User"
msgstr "Verknüpfte*r Benutzer*in"
@ -532,7 +531,7 @@ msgstr "Verknüpfte*r Benutzer*in"
msgid "No member linked"
msgstr "Kein Mitglied verknüpft"
#: lib/mv_web/live/member_live/show.ex:73
#: lib/mv_web/live/member_live/show.ex:72
#, elixir-autogen, elixir-format
msgid "No user linked"
msgstr "Keine*r Benutzer*in verknüpft"
@ -562,7 +561,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:33
#: lib/mv_web/live/member_live/index.html.heex:34
#, elixir-autogen, elixir-format
msgid "Search..."
msgstr "Suchen..."
@ -578,7 +577,7 @@ msgstr "Benutzer*innen"
msgid "Click to sort"
msgstr "Klicke um zu sortieren"
#: lib/mv_web/live/member_live/index.html.heex:81
#: lib/mv_web/live/member_live/index.html.heex:89
#, elixir-autogen, elixir-format
msgid "First name"
msgstr "Vorname"
@ -619,8 +618,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:59
#: lib/mv_web/live/member_live/show.ex:78
#: lib/mv_web/live/member_live/form.ex:58
#: lib/mv_web/live/member_live/show.ex:77
#, elixir-autogen, elixir-format
msgid "Custom Field Values"
msgstr "Benutzerdefinierte Feldwerte"
@ -790,7 +789,7 @@ msgstr "Mitglied entverknüpfen"
msgid "Unlinking scheduled"
msgstr "Entverknüpfung geplant"
#: lib/mv_web/live/member_live/index.ex:164
#: lib/mv_web/live/member_live/index.ex:165
#, elixir-autogen, elixir-format
msgid "Copied %{count} email address to clipboard"
msgid_plural "Copied %{count} email addresses to clipboard"
@ -807,12 +806,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:153
#: lib/mv_web/live/member_live/index.ex:154
#, elixir-autogen, elixir-format
msgid "No email addresses found"
msgstr "Keine E-Mail-Adressen gefunden"
#: lib/mv_web/live/member_live/index.ex:150
#: lib/mv_web/live/member_live/index.ex:151
#, elixir-autogen, elixir-format
msgid "No members selected"
msgstr "Keine Mitglieder ausgewählt"
@ -827,7 +826,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:173
#: lib/mv_web/live/member_live/index.ex:174
#, 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"
@ -845,6 +844,28 @@ 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"

View file

@ -17,7 +17,7 @@ msgstr ""
msgid "Actions"
msgstr ""
#: lib/mv_web/live/member_live/index.html.heex:227
#: lib/mv_web/live/member_live/index.html.heex:243
#: lib/mv_web/live/user_live/index.html.heex:72
#, 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:54
#: lib/mv_web/live/member_live/index.html.heex:171
#: lib/mv_web/live/member_live/show.ex:59
#: 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
#, 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:229
#: lib/mv_web/live/member_live/index.html.heex:245
#: 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:221
#: lib/mv_web/live/member_live/index.html.heex:237
#: lib/mv_web/live/user_live/form.ex:265
#: lib/mv_web/live/user_live/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:117
#: lib/mv_web/live/member_live/show.ex:116
#, 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:99
#: lib/mv_web/live/member_live/index.html.heex:107
#: 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:51
#: lib/mv_web/live/member_live/index.html.heex:207
#: lib/mv_web/live/member_live/show.ex:56
#: 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
#, 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:218
#: lib/mv_web/live/member_live/index.html.heex:234
#: lib/mv_web/live/user_live/index.html.heex:63
#, elixir-autogen, elixir-format
msgid "Show"
@ -113,55 +113,52 @@ msgstr ""
msgid "close"
msgstr ""
#: 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
#: lib/mv_web/live/member_live/form.ex:51
#: lib/mv_web/live/member_live/show.ex:56
#, elixir-autogen, elixir-format
msgid "Exit Date"
msgstr ""
#: 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
#: 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
#, 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:53
#: lib/mv_web/live/member_live/show.ex:58
#: lib/mv_web/live/member_live/form.ex:52
#: lib/mv_web/live/member_live/show.ex:57
#, 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:49
#: lib/mv_web/live/member_live/show.ex:52
#: 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
#, elixir-autogen, elixir-format
msgid "Paid"
msgstr ""
#: 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
#: 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
#, elixir-autogen, elixir-format
msgid "Phone Number"
msgstr ""
#: 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
#: 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
#, elixir-autogen, elixir-format
msgid "Postal Code"
msgstr ""
#: lib/mv_web/live/member_live/form.ex:80
#: lib/mv_web/live/member_live/form.ex:79
#, elixir-autogen, elixir-format
msgid "Save Member"
msgstr ""
@ -169,15 +166,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:79
#: lib/mv_web/live/member_live/form.ex:78
#: lib/mv_web/live/user_live/form.ex:248
#, elixir-autogen, elixir-format
msgid "Saving..."
msgstr ""
#: 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
#: 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
#, elixir-autogen, elixir-format
msgid "Street"
msgstr ""
@ -187,13 +184,14 @@ 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:53
#: lib/mv_web/live/member_live/show.ex:52
#, elixir-autogen, elixir-format
msgid "No"
msgstr ""
#: lib/mv_web/live/member_live/show.ex:116
#: lib/mv_web/live/member_live/show.ex:115
#, elixir-autogen, elixir-format
msgid "Show Member"
msgstr ""
@ -203,22 +201,23 @@ 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:53
#: lib/mv_web/live/member_live/show.ex:52
#, 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:138
#: lib/mv_web/live/member_live/form.ex:137
#, 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:139
#: lib/mv_web/live/member_live/form.ex:138
#, elixir-autogen, elixir-format
msgid "update"
msgstr ""
@ -228,7 +227,7 @@ msgstr ""
msgid "Incorrect email or password"
msgstr ""
#: lib/mv_web/live/member_live/form.ex:145
#: lib/mv_web/live/member_live/form.ex:144
#, elixir-autogen, elixir-format
msgid "Member %{action} successfully"
msgstr ""
@ -261,7 +260,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:82
#: lib/mv_web/live/member_live/form.ex:81
#: lib/mv_web/live/user_live/form.ex:251
#, elixir-autogen, elixir-format
msgid "Cancel"
@ -372,12 +371,12 @@ msgstr ""
msgid "Required"
msgstr ""
#: lib/mv_web/live/member_live/index.html.heex:55
#: lib/mv_web/live/member_live/index.html.heex:63
#, elixir-autogen, elixir-format
msgid "Select all members"
msgstr ""
#: lib/mv_web/live/member_live/index.html.heex:69
#: lib/mv_web/live/member_live/index.html.heex:77
#, elixir-autogen, elixir-format
msgid "Select member"
msgstr ""
@ -522,7 +521,7 @@ msgstr ""
msgid "Linked Member"
msgstr ""
#: lib/mv_web/live/member_live/show.ex:63
#: lib/mv_web/live/member_live/show.ex:62
#, elixir-autogen, elixir-format
msgid "Linked User"
msgstr ""
@ -533,7 +532,7 @@ msgstr ""
msgid "No member linked"
msgstr ""
#: lib/mv_web/live/member_live/show.ex:73
#: lib/mv_web/live/member_live/show.ex:72
#, elixir-autogen, elixir-format
msgid "No user linked"
msgstr ""
@ -563,7 +562,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:33
#: lib/mv_web/live/member_live/index.html.heex:34
#, elixir-autogen, elixir-format
msgid "Search..."
msgstr ""
@ -579,7 +578,7 @@ msgstr ""
msgid "Click to sort"
msgstr ""
#: lib/mv_web/live/member_live/index.html.heex:81
#: lib/mv_web/live/member_live/index.html.heex:89
#, elixir-autogen, elixir-format
msgid "First name"
msgstr ""
@ -620,8 +619,8 @@ msgstr ""
msgid "Choose a custom field"
msgstr ""
#: lib/mv_web/live/member_live/form.ex:59
#: lib/mv_web/live/member_live/show.ex:78
#: lib/mv_web/live/member_live/form.ex:58
#: lib/mv_web/live/member_live/show.ex:77
#, elixir-autogen, elixir-format
msgid "Custom Field Values"
msgstr ""
@ -791,7 +790,7 @@ msgstr ""
msgid "Unlinking scheduled"
msgstr ""
#: lib/mv_web/live/member_live/index.ex:164
#: lib/mv_web/live/member_live/index.ex:165
#, elixir-autogen, elixir-format
msgid "Copied %{count} email address to clipboard"
msgid_plural "Copied %{count} email addresses to clipboard"
@ -808,12 +807,12 @@ msgstr ""
msgid "Copy emails"
msgstr ""
#: lib/mv_web/live/member_live/index.ex:153
#: lib/mv_web/live/member_live/index.ex:154
#, elixir-autogen, elixir-format
msgid "No email addresses found"
msgstr ""
#: lib/mv_web/live/member_live/index.ex:150
#: lib/mv_web/live/member_live/index.ex:151
#, elixir-autogen, elixir-format
msgid "No members selected"
msgstr ""
@ -828,7 +827,7 @@ msgstr ""
msgid "Open in email program"
msgstr ""
#: lib/mv_web/live/member_live/index.ex:173
#: lib/mv_web/live/member_live/index.ex:174
#, elixir-autogen, elixir-format
msgid "Tip: Paste email addresses into the BCC field for privacy compliance"
msgstr ""
@ -846,6 +845,28 @@ 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"

View file

@ -17,7 +17,7 @@ msgstr ""
msgid "Actions"
msgstr ""
#: lib/mv_web/live/member_live/index.html.heex:227
#: lib/mv_web/live/member_live/index.html.heex:243
#: lib/mv_web/live/user_live/index.html.heex:72
#, 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:54
#: lib/mv_web/live/member_live/index.html.heex:171
#: lib/mv_web/live/member_live/show.ex:59
#: 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
#, 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:229
#: lib/mv_web/live/member_live/index.html.heex:245
#: 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:221
#: lib/mv_web/live/member_live/index.html.heex:237
#: lib/mv_web/live/user_live/form.ex:265
#: lib/mv_web/live/user_live/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:117
#: lib/mv_web/live/member_live/show.ex:116
#, 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:99
#: lib/mv_web/live/member_live/index.html.heex:107
#: 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:51
#: lib/mv_web/live/member_live/index.html.heex:207
#: lib/mv_web/live/member_live/show.ex:56
#: 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
#, 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:218
#: lib/mv_web/live/member_live/index.html.heex:234
#: lib/mv_web/live/user_live/index.html.heex:63
#, elixir-autogen, elixir-format
msgid "Show"
@ -113,55 +113,52 @@ msgstr ""
msgid "close"
msgstr ""
#: 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
#: lib/mv_web/live/member_live/form.ex:51
#: lib/mv_web/live/member_live/show.ex:56
#, elixir-autogen, elixir-format
msgid "Exit Date"
msgstr ""
#: 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
#: 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
#, 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:53
#: lib/mv_web/live/member_live/show.ex:58
#: lib/mv_web/live/member_live/form.ex:52
#: lib/mv_web/live/member_live/show.ex:57
#, 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:49
#: lib/mv_web/live/member_live/show.ex:52
#: 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
#, elixir-autogen, elixir-format
msgid "Paid"
msgstr ""
#: 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
#: 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
#, elixir-autogen, elixir-format
msgid "Phone Number"
msgstr ""
#: 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
#: 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
#, elixir-autogen, elixir-format
msgid "Postal Code"
msgstr ""
#: lib/mv_web/live/member_live/form.ex:80
#: lib/mv_web/live/member_live/form.ex:79
#, elixir-autogen, elixir-format, fuzzy
msgid "Save Member"
msgstr ""
@ -169,15 +166,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:79
#: lib/mv_web/live/member_live/form.ex:78
#: lib/mv_web/live/user_live/form.ex:248
#, elixir-autogen, elixir-format
msgid "Saving..."
msgstr ""
#: 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
#: 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
#, elixir-autogen, elixir-format
msgid "Street"
msgstr ""
@ -187,13 +184,14 @@ 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:53
#: lib/mv_web/live/member_live/show.ex:52
#, elixir-autogen, elixir-format
msgid "No"
msgstr ""
#: lib/mv_web/live/member_live/show.ex:116
#: lib/mv_web/live/member_live/show.ex:115
#, elixir-autogen, elixir-format, fuzzy
msgid "Show Member"
msgstr ""
@ -203,22 +201,23 @@ 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:53
#: lib/mv_web/live/member_live/show.ex:52
#, 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:138
#: lib/mv_web/live/member_live/form.ex:137
#, 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:139
#: lib/mv_web/live/member_live/form.ex:138
#, elixir-autogen, elixir-format
msgid "update"
msgstr ""
@ -228,7 +227,7 @@ msgstr ""
msgid "Incorrect email or password"
msgstr ""
#: lib/mv_web/live/member_live/form.ex:145
#: lib/mv_web/live/member_live/form.ex:144
#, elixir-autogen, elixir-format
msgid "Member %{action} successfully"
msgstr ""
@ -261,7 +260,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:82
#: lib/mv_web/live/member_live/form.ex:81
#: lib/mv_web/live/user_live/form.ex:251
#, elixir-autogen, elixir-format
msgid "Cancel"
@ -372,12 +371,12 @@ msgstr ""
msgid "Required"
msgstr ""
#: lib/mv_web/live/member_live/index.html.heex:55
#: lib/mv_web/live/member_live/index.html.heex:63
#, elixir-autogen, elixir-format
msgid "Select all members"
msgstr ""
#: lib/mv_web/live/member_live/index.html.heex:69
#: lib/mv_web/live/member_live/index.html.heex:77
#, elixir-autogen, elixir-format
msgid "Select member"
msgstr ""
@ -522,7 +521,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:63
#: lib/mv_web/live/member_live/show.ex:62
#, elixir-autogen, elixir-format
msgid "Linked User"
msgstr ""
@ -533,7 +532,7 @@ msgstr ""
msgid "No member linked"
msgstr ""
#: lib/mv_web/live/member_live/show.ex:73
#: lib/mv_web/live/member_live/show.ex:72
#, elixir-autogen, elixir-format
msgid "No user linked"
msgstr ""
@ -563,7 +562,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:33
#: lib/mv_web/live/member_live/index.html.heex:34
#, elixir-autogen, elixir-format
msgid "Search..."
msgstr ""
@ -579,7 +578,7 @@ msgstr ""
msgid "Click to sort"
msgstr ""
#: lib/mv_web/live/member_live/index.html.heex:81
#: lib/mv_web/live/member_live/index.html.heex:89
#, elixir-autogen, elixir-format, fuzzy
msgid "First name"
msgstr ""
@ -620,8 +619,8 @@ msgstr ""
msgid "Choose a custom field"
msgstr ""
#: lib/mv_web/live/member_live/form.ex:59
#: lib/mv_web/live/member_live/show.ex:78
#: lib/mv_web/live/member_live/form.ex:58
#: lib/mv_web/live/member_live/show.ex:77
#, elixir-autogen, elixir-format
msgid "Custom Field Values"
msgstr ""
@ -791,7 +790,7 @@ msgstr ""
msgid "Unlinking scheduled"
msgstr ""
#: lib/mv_web/live/member_live/index.ex:164
#: lib/mv_web/live/member_live/index.ex:165
#, elixir-autogen, elixir-format
msgid "Copied %{count} email address to clipboard"
msgid_plural "Copied %{count} email addresses to clipboard"
@ -808,12 +807,12 @@ msgstr ""
msgid "Copy emails"
msgstr ""
#: lib/mv_web/live/member_live/index.ex:153
#: lib/mv_web/live/member_live/index.ex:154
#, elixir-autogen, elixir-format
msgid "No email addresses found"
msgstr ""
#: lib/mv_web/live/member_live/index.ex:150
#: lib/mv_web/live/member_live/index.ex:151
#, elixir-autogen, elixir-format, fuzzy
msgid "No members selected"
msgstr ""
@ -828,7 +827,7 @@ msgstr ""
msgid "Open in email program"
msgstr ""
#: lib/mv_web/live/member_live/index.ex:173
#: lib/mv_web/live/member_live/index.ex:174
#, elixir-autogen, elixir-format
msgid "Tip: Paste email addresses into the BCC field for privacy compliance"
msgstr ""
@ -846,6 +845,28 @@ 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"

View file

@ -0,0 +1,69 @@
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

View file

@ -112,7 +112,6 @@ 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",
@ -125,7 +124,6 @@ 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",
@ -139,7 +137,6 @@ 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",
@ -151,7 +148,6 @@ 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",
@ -186,7 +182,6 @@ 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",
@ -202,7 +197,6 @@ 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",

View file

@ -6,7 +6,6 @@ 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",
@ -43,12 +42,6 @@ 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")

View file

@ -0,0 +1,183 @@
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

View file

@ -9,7 +9,8 @@ defmodule MvWeb.MemberLive.IndexCustomFieldsDisplayTest do
- Custom field values are correctly formatted for different types
- Members without custom field values show empty cell or "-"
"""
use MvWeb.ConnCase, async: true
# async: false to prevent PostgreSQL deadlocks when creating members and custom fields
use MvWeb.ConnCase, async: false
import Phoenix.LiveViewTest
require Ash.Query

View file

@ -469,4 +469,221 @@ 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