diff --git a/CHANGELOG.md b/CHANGELOG.md index 71d9147..74df997 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,14 +12,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - PostgreSQL trigram-based member search with typo tolerance - WCAG 2.1 AA compliant autocomplete dropdown with ARIA support - Bilingual UI (German/English) for member linking workflow -- **Bulk email copy feature** - Copy email addresses of selected members to clipboard (#230) - - Email format: "First Last " with semicolon separator (compatible with email clients) - - CopyToClipboard JavaScript hook with fallback for older browsers - - Button shows count of visible selected members (respects search/filter) - - German/English translations ### Fixed - Email validation false positive when linking user and member with identical emails (#168 Problem #4) - Relationship data extraction from Ash manage_relationship during validation -- Copy button count now shows only visible selected members when filtering diff --git a/assets/js/app.js b/assets/js/app.js index 883ca30..e55a06d 100644 --- a/assets/js/app.js +++ b/assets/js/app.js @@ -27,33 +27,6 @@ let csrfToken = document.querySelector("meta[name='csrf-token']").getAttribute(" // Hooks for LiveView components let Hooks = {} -// CopyToClipboard hook: Copies text to clipboard when triggered by server event -Hooks.CopyToClipboard = { - mounted() { - this.handleEvent("copy_to_clipboard", ({text}) => { - if (navigator.clipboard) { - navigator.clipboard.writeText(text).catch(err => { - console.error("Clipboard write failed:", err) - }) - } else { - // Fallback for older browsers - const textArea = document.createElement("textarea") - textArea.value = text - textArea.style.position = "fixed" - textArea.style.left = "-999999px" - document.body.appendChild(textArea) - textArea.select() - try { - document.execCommand("copy") - } catch (err) { - console.error("Fallback clipboard copy failed:", err) - } - document.body.removeChild(textArea) - } - }) - } -} - // ComboBox hook: Prevents form submission when Enter is pressed in dropdown Hooks.ComboBox = { mounted() { diff --git a/docs/development-progress-log.md b/docs/development-progress-log.md index 629987e..5669a19 100644 --- a/docs/development-progress-log.md +++ b/docs/development-progress-log.md @@ -1327,33 +1327,6 @@ end --- -## Session: Bulk Email Copy Feature (2025-12-02) - -### Feature Summary -Implemented bulk email copy functionality for selected members (#230). Users can select members and copy their email addresses to clipboard. - -**Key Features:** -- Copy button appears only when visible members are selected -- Email format: `First Last ` with semicolon separator (email client compatible) -- Button shows count of visible selected members (respects search/filter) -- CopyToClipboard JavaScript hook with clipboard API + fallback for older browsers -- Bilingual UI (English/German) - -### Key Decisions - -1. **Email Format:** "First Last " with semicolon - standard for all major email clients -2. **Visible Member Count:** Button shows only visible selected members, not total selected (better UX when filtering) -3. **Server→Client:** Used `push_event/3` - server formats data, client handles clipboard - -### Files Changed -- `lib/mv_web/live/member_live/index.ex` - Event handler, helper function -- `lib/mv_web/live/member_live/index.html.heex` - Copy button -- `assets/js/app.js` - CopyToClipboard hook -- `test/mv_web/member_live/index_test.exs` - 9 new tests -- `priv/gettext/de/LC_MESSAGES/default.po` - German translations - ---- - ## Session: User-Member Linking UI Enhancement (2025-01-13) ### Feature Summary @@ -1586,8 +1559,8 @@ This project demonstrates a modern Phoenix application built with: --- -**Document Version:** 1.3 -**Last Updated:** 2025-12-02 +**Document Version:** 1.2 +**Last Updated:** 2025-11-27 **Maintainer:** Development Team **Status:** Living Document (update as project evolves) diff --git a/docs/feature-roadmap.md b/docs/feature-roadmap.md index 60432d0..2313fd7 100644 --- a/docs/feature-roadmap.md +++ b/docs/feature-roadmap.md @@ -65,7 +65,6 @@ - ✅ Sorting by basic fields - ✅ User-Member linking (optional 1:1) - ✅ Email synchronization between User and Member -- ✅ **Bulk email copy** - Copy selected members' email addresses to clipboard (Issue #230) **Closed Issues:** - ✅ [#162](https://git.local-it.org/local-it/mitgliederverwaltung/issues/162) - Fuzzy and substring search (closed 2025-11-12) diff --git a/lib/membership/member.ex b/lib/membership/member.ex index bcd505e..da69861 100644 --- a/lib/membership/member.ex +++ b/lib/membership/member.ex @@ -42,10 +42,6 @@ defmodule Mv.Membership.Member do @member_search_limit 10 @default_similarity_threshold 0.2 - # Use constants from Mv.Constants for member fields - # This ensures consistency across the codebase - @member_fields Mv.Constants.member_fields() - postgres do table "members" repo Mv.Repo @@ -62,7 +58,21 @@ defmodule Mv.Membership.Member do # user_id is NOT in accept list to prevent direct foreign key manipulation argument :user, :map, allow_nil?: true - accept @member_fields + accept [ + :first_name, + :last_name, + :email, + :birth_date, + :paid, + :phone_number, + :join_date, + :exit_date, + :notes, + :city, + :street, + :house_number, + :postal_code + ] change manage_relationship(:custom_field_values, type: :create) @@ -95,7 +105,21 @@ defmodule Mv.Membership.Member do # user_id is NOT in accept list to prevent direct foreign key manipulation argument :user, :map, allow_nil?: true - accept @member_fields + accept [ + :first_name, + :last_name, + :email, + :birth_date, + :paid, + :phone_number, + :join_date, + :exit_date, + :notes, + :city, + :street, + :house_number, + :postal_code + ] change manage_relationship(:custom_field_values, on_match: :update, on_no_match: :create) diff --git a/lib/membership/membership.ex b/lib/membership/membership.ex index f5a708b..cb3691b 100644 --- a/lib/membership/membership.ex +++ b/lib/membership/membership.ex @@ -53,7 +53,6 @@ defmodule Mv.Membership do # It's only used internally as fallback in get_settings/0 # Settings should be created via seed script define :update_settings, action: :update - define :update_member_field_visibility, action: :update_member_field_visibility end end @@ -124,37 +123,4 @@ defmodule Mv.Membership do |> Ash.Changeset.for_update(:update, attrs) |> Ash.update(domain: __MODULE__) end - - @doc """ - Updates the member field visibility configuration. - - This is a specialized action for updating only the member field visibility settings. - It validates that all keys are valid member fields and all values are booleans. - - ## Parameters - - - `settings` - The settings record to update - - `visibility_config` - A map of member field names (strings) to boolean visibility values - (e.g., `%{"street" => false, "house_number" => false}`) - - ## Returns - - - `{:ok, updated_settings}` - Successfully updated settings - - `{:error, error}` - Validation or update error - - ## Examples - - iex> {:ok, settings} = Mv.Membership.get_settings() - iex> {:ok, updated} = Mv.Membership.update_member_field_visibility(settings, %{"street" => false, "house_number" => false}) - iex> updated.member_field_visibility - %{"street" => false, "house_number" => false} - - """ - def update_member_field_visibility(settings, visibility_config) do - settings - |> Ash.Changeset.for_update(:update_member_field_visibility, %{ - member_field_visibility: visibility_config - }) - |> Ash.update(domain: __MODULE__) - end end diff --git a/lib/membership/setting.ex b/lib/membership/setting.ex index 52c0328..38624dc 100644 --- a/lib/membership/setting.ex +++ b/lib/membership/setting.ex @@ -9,8 +9,6 @@ defmodule Mv.Membership.Setting do ## Attributes - `club_name` - The name of the association/club (required, cannot be empty) - - `member_field_visibility` - JSONB map storing visibility configuration for member fields - (e.g., `%{"street" => false, "house_number" => false}`). Fields not in the map default to `true`. ## Singleton Pattern This resource uses a singleton pattern - there should only be one settings record. @@ -30,9 +28,6 @@ defmodule Mv.Membership.Setting do # Update club name {:ok, updated} = Mv.Membership.update_settings(settings, %{club_name: "New Name"}) - - # Update member field visibility - {:ok, updated} = Mv.Membership.update_member_field_visibility(settings, %{"street" => false, "house_number" => false}) """ use Ash.Resource, domain: Mv.Membership, @@ -54,65 +49,18 @@ defmodule Mv.Membership.Setting do # Used only as fallback in get_settings/0 if settings don't exist # Settings should normally be created via seed script create :create do - accept [:club_name, :member_field_visibility] + accept [:club_name] end update :update do primary? true - require_atomic? false - accept [:club_name, :member_field_visibility] - end - - update :update_member_field_visibility do - description "Updates the visibility configuration for member fields in the overview" - require_atomic? false - accept [:member_field_visibility] + accept [:club_name] end end validations do validate present(:club_name), on: [:create, :update] validate string_length(:club_name, min: 1), on: [:create, :update] - - # Validate member_field_visibility map structure and content - validate fn changeset, _context -> - visibility = Ash.Changeset.get_attribute(changeset, :member_field_visibility) - - if visibility && is_map(visibility) do - # Validate all values are booleans - invalid_values = - Enum.filter(visibility, fn {_key, value} -> - not is_boolean(value) - end) - - # Validate all keys are valid member fields - valid_field_strings = Mv.Constants.member_fields() |> Enum.map(&Atom.to_string/1) - - invalid_keys = - Enum.filter(visibility, fn {key, _value} -> - key not in valid_field_strings - end) - |> Enum.map(fn {key, _value} -> key end) - - cond do - not Enum.empty?(invalid_values) -> - {:error, - field: :member_field_visibility, - message: "All values in member_field_visibility must be booleans"} - - not Enum.empty?(invalid_keys) -> - {:error, - field: :member_field_visibility, - message: "Invalid member field keys: #{inspect(invalid_keys)}"} - - true -> - :ok - end - else - :ok - end - end, - on: [:create, :update] end attributes do @@ -127,12 +75,6 @@ defmodule Mv.Membership.Setting do min_length: 1 ] - attribute :member_field_visibility, :map, - allow_nil?: true, - public?: true, - description: - "Configuration for member field visibility in overview (JSONB map). Keys are member field names (atoms), values are booleans." - timestamps() end end diff --git a/lib/mv/constants.ex b/lib/mv/constants.ex deleted file mode 100644 index cd8d3a4..0000000 --- a/lib/mv/constants.ex +++ /dev/null @@ -1,23 +0,0 @@ -defmodule Mv.Constants do - @moduledoc """ - Module for defining constants and atoms. - """ - - @member_fields [ - :first_name, - :last_name, - :email, - :birth_date, - :paid, - :phone_number, - :join_date, - :exit_date, - :notes, - :city, - :street, - :house_number, - :postal_code - ] - - def member_fields, do: @member_fields -end diff --git a/lib/mv_web/components/core_components.ex b/lib/mv_web/components/core_components.ex index 54a5a64..da9e9c9 100644 --- a/lib/mv_web/components/core_components.ex +++ b/lib/mv_web/components/core_components.ex @@ -42,11 +42,7 @@ defmodule MvWeb.CoreComponents do attr :id, :string, doc: "the optional id of flash container" attr :flash, :map, default: %{}, doc: "the map of flash messages to display" attr :title, :string, default: nil - - attr :kind, :atom, - values: [:info, :error, :success, :warning], - doc: "used for styling and flash lookup" - + attr :kind, :atom, values: [:info, :error], doc: "used for styling and flash lookup" attr :rest, :global, doc: "the arbitrary HTML attributes to add to the flash container" slot :inner_block, doc: "the optional inner block that renders the flash message" @@ -66,20 +62,16 @@ defmodule MvWeb.CoreComponents do
<.icon :if={@kind == :info} name="hero-information-circle" class="size-5 shrink-0" /> <.icon :if={@kind == :error} name="hero-exclamation-circle" class="size-5 shrink-0" /> - <.icon :if={@kind == :success} name="hero-check-circle" class="size-5 shrink-0" /> - <.icon :if={@kind == :warning} name="hero-information-circle" class="size-5 shrink-0" />

{@title}

{msg}

-
diff --git a/lib/mv_web/components/layouts.ex b/lib/mv_web/components/layouts.ex index 487a01f..b7f7568 100644 --- a/lib/mv_web/components/layouts.ex +++ b/lib/mv_web/components/layouts.ex @@ -65,9 +65,7 @@ defmodule MvWeb.Layouts do def flash_group(assigns) do ~H""" -
- <.flash kind={:success} flash={@flash} /> - <.flash kind={:warning} flash={@flash} /> +
<.flash kind={:info} flash={@flash} /> <.flash kind={:error} flash={@flash} /> diff --git a/lib/mv_web/live/member_live/index.ex b/lib/mv_web/live/member_live/index.ex index 4d444b9..85ee4fb 100644 --- a/lib/mv_web/live/member_live/index.ex +++ b/lib/mv_web/live/member_live/index.ex @@ -18,7 +18,6 @@ defmodule MvWeb.MemberLive.Index do - `delete` - Remove a member from the database - `select_member` - Toggle individual member selection - `select_all` - Toggle selection of all visible members - - `copy_emails` - Copy email addresses of selected members to clipboard ## Implementation Notes - Search uses PostgreSQL full-text search (plainto_tsquery) @@ -30,18 +29,11 @@ defmodule MvWeb.MemberLive.Index do require Ash.Query import Ash.Expr - alias Mv.Membership alias MvWeb.MemberLive.Index.Formatter # Prefix used in sort field names for custom fields (e.g., "custom_field_") @custom_field_prefix "custom_field_" - # Member fields that are loaded for the overview - # Uses constants from Mv.Constants to ensure consistency - # Note: :id is always included for member identification - # All member fields are loaded, but visibility is controlled via settings - @overview_fields [:id | Mv.Constants.member_fields()] - @doc """ Initializes the LiveView state. @@ -60,23 +52,14 @@ defmodule MvWeb.MemberLive.Index do |> Ash.Query.sort(name: :asc) |> Ash.read!() - # Load settings once to avoid N+1 queries - settings = - case Membership.get_settings() do - {:ok, s} -> s - # Fallback if settings can't be loaded - {:error, _} -> %{member_field_visibility: %{}} - end - socket = socket |> assign(:page_title, gettext("Members")) |> assign(:query, "") |> assign_new(:sort_field, fn -> :first_name end) |> assign_new(:sort_order, fn -> :asc end) - |> assign(:selected_members, MapSet.new()) + |> assign(:selected_members, []) |> assign(:custom_fields_visible, custom_fields_visible) - |> assign(:member_fields_visible, get_visible_member_fields(settings)) # We call handle params to use the query from the URL {:ok, socket} @@ -108,10 +91,10 @@ defmodule MvWeb.MemberLive.Index do @impl true def handle_event("select_member", %{"id" => id}, socket) do selected = - if MapSet.member?(socket.assigns.selected_members, id) do - MapSet.delete(socket.assigns.selected_members, id) + if id in socket.assigns.selected_members do + List.delete(socket.assigns.selected_members, id) else - MapSet.put(socket.assigns.selected_members, id) + [id | socket.assigns.selected_members] end {:noreply, assign(socket, :selected_members, selected)} @@ -119,11 +102,13 @@ defmodule MvWeb.MemberLive.Index do @impl true def handle_event("select_all", _params, socket) do - all_ids = socket.assigns.members |> Enum.map(& &1.id) |> MapSet.new() + members = socket.assigns.members + + all_ids = Enum.map(members, & &1.id) selected = - if MapSet.equal?(socket.assigns.selected_members, all_ids) do - MapSet.new() + if Enum.sort(socket.assigns.selected_members) == Enum.sort(all_ids) do + [] else all_ids end @@ -131,52 +116,6 @@ defmodule MvWeb.MemberLive.Index do {:noreply, assign(socket, :selected_members, selected)} end - @impl true - def handle_event("copy_emails", _params, socket) do - selected_ids = socket.assigns.selected_members - - # Filter members that are in the selection and have email addresses - formatted_emails = - socket.assigns.members - |> Enum.filter(fn member -> - MapSet.member?(selected_ids, member.id) && member.email && member.email != "" - end) - |> Enum.map(&format_member_email/1) - - email_count = length(formatted_emails) - - cond do - MapSet.size(selected_ids) == 0 -> - {:noreply, put_flash(socket, :error, gettext("No members selected"))} - - email_count == 0 -> - {:noreply, put_flash(socket, :error, gettext("No email addresses found"))} - - true -> - # RFC 5322 uses comma as separator for email address lists - email_string = Enum.join(formatted_emails, ", ") - - socket = - socket - |> push_event("copy_to_clipboard", %{text: email_string}) - |> put_flash( - :success, - ngettext( - "Copied %{count} email address to clipboard", - "Copied %{count} email addresses to clipboard", - email_count, - count: email_count - ) - ) - |> put_flash( - :warning, - gettext("Tip: Paste email addresses into the BCC field for privacy compliance") - ) - - {:noreply, socket} - end - end - # ----------------------------------------------------------------- # Handle Infos from Child Components # ----------------------------------------------------------------- @@ -374,7 +313,18 @@ defmodule MvWeb.MemberLive.Index do query = Mv.Membership.Member |> Ash.Query.new() - |> Ash.Query.select(@overview_fields) + |> Ash.Query.select([ + :id, + :first_name, + :last_name, + :email, + :street, + :house_number, + :postal_code, + :city, + :phone_number, + :join_date + ]) # Load custom field values for visible custom fields custom_field_ids_list = Enum.map(socket.assigns.custom_fields_visible, & &1.id) @@ -483,13 +433,18 @@ defmodule MvWeb.MemberLive.Index do defp maybe_sort(query, _, _, _), do: {query, false} # Validate that a field is sortable - # Uses member fields from constants, but excludes fields that don't make sense to sort - # (e.g., :notes is too long, :paid is boolean and not very useful for sorting) defp valid_sort_field?(field) when is_atom(field) do - # All member fields are sortable, but we exclude some that don't make sense - # :id is not in member_fields, but we don't want to sort by it anyway - non_sortable_fields = [:notes, :paid] - valid_fields = Mv.Constants.member_fields() -- non_sortable_fields + valid_fields = [ + :first_name, + :last_name, + :email, + :street, + :house_number, + :postal_code, + :city, + :phone_number, + :join_date + ] field in valid_fields or custom_field_sort?(field) end @@ -778,50 +733,4 @@ defmodule MvWeb.MemberLive.Index do nil end end - - # Formats a member's email in the format "First Last " - # Used for copy_emails feature to create email-client-friendly format. - defp format_member_email(member) do - first_name = member.first_name || "" - last_name = member.last_name || "" - - name = - [first_name, last_name] - |> Enum.filter(&(&1 != "")) - |> Enum.join(" ") - - if name == "" do - member.email - else - "#{name} <#{member.email}>" - end - end - - # Gets the list of member fields that should be visible in the overview. - # - # Reads the visibility configuration from Settings and returns only the fields - # where show_in_overview is true. Fields not configured in settings default to true. - # - # Performance: This function uses the already-loaded settings to avoid N+1 queries. - # Settings should be loaded once in mount/3 and passed to this function. - # - # Parameters: - # - `settings` - The settings struct loaded from the database - # - # Returns a list of atoms representing visible member field names. - # - # Fields are read from the global Constants module. - @spec get_visible_member_fields(map()) :: [atom()] - defp get_visible_member_fields(settings) do - # Get all eligible fields from the global constants - all_fields = Mv.Constants.member_fields() - - # JSONB stores keys as strings - visibility_config = settings.member_field_visibility || %{} - - # Filter to only return visible fields - Enum.filter(all_fields, fn field -> - Map.get(visibility_config, Atom.to_string(field), true) - end) - end end diff --git a/lib/mv_web/live/member_live/index.html.heex b/lib/mv_web/live/member_live/index.html.heex index 55b0a20..67fa804 100644 --- a/lib/mv_web/live/member_live/index.html.heex +++ b/lib/mv_web/live/member_live/index.html.heex @@ -2,24 +2,6 @@ <.header> {gettext("Members")} <:actions> - <.button - :if={Enum.any?(@members, &MapSet.member?(@selected_members, &1.id))} - id="copy-emails-btn" - phx-hook="CopyToClipboard" - phx-click="copy_emails" - aria-label={gettext("Copy email addresses of selected members")} - > - <.icon name="hero-clipboard-document" /> - {gettext("Copy emails")} ({Enum.count(@members, &MapSet.member?(@selected_members, &1.id))}) - - <.button - :if={Enum.any?(@members, &MapSet.member?(@selected_members, &1.id))} - href={"mailto:?bcc=#{@members |> Enum.filter(&(MapSet.member?(@selected_members, &1.id) && &1.email)) |> Enum.map(& &1.email) |> Enum.join(",")}"} - aria-label={gettext("Open email program with BCC recipients")} - > - <.icon name="hero-envelope" /> - {gettext("Open in email program")} - <.button variant="primary" navigate={~p"/members/new"}> <.icon name="hero-plus" /> {gettext("New Member")} @@ -51,7 +33,7 @@ type="checkbox" name="select_all" phx-click="select_all" - checked={MapSet.equal?(@selected_members, @members |> Enum.map(& &1.id) |> MapSet.new())} + checked={Enum.sort(@selected_members) == Enum.map(@members, & &1.id) |> Enum.sort()} aria-label={gettext("Select all members")} role="checkbox" /> @@ -63,7 +45,7 @@ name={member.id} phx-click="select_member" phx-value-id={member.id} - checked={MapSet.member?(@selected_members, member.id)} + checked={member.id in @selected_members} phx-capture-click phx-stop-propagation aria-label={gettext("Select member")} @@ -89,7 +71,6 @@ <:col :let={member} - :if={:email in @member_fields_visible} label={ ~H""" <.live_component @@ -107,7 +88,6 @@ <:col :let={member} - :if={:street in @member_fields_visible} label={ ~H""" <.live_component @@ -125,7 +105,6 @@ <:col :let={member} - :if={:house_number in @member_fields_visible} label={ ~H""" <.live_component @@ -143,7 +122,6 @@ <:col :let={member} - :if={:postal_code in @member_fields_visible} label={ ~H""" <.live_component @@ -161,7 +139,6 @@ <:col :let={member} - :if={:city in @member_fields_visible} label={ ~H""" <.live_component @@ -179,7 +156,6 @@ <:col :let={member} - :if={:phone_number in @member_fields_visible} label={ ~H""" <.live_component @@ -197,7 +173,6 @@ <:col :let={member} - :if={:join_date in @member_fields_visible} label={ ~H""" <.live_component diff --git a/priv/gettext/de/LC_MESSAGES/default.po b/priv/gettext/de/LC_MESSAGES/default.po index 68afafc..68723ba 100644 --- a/priv/gettext/de/LC_MESSAGES/default.po +++ b/priv/gettext/de/LC_MESSAGES/default.po @@ -10,37 +10,37 @@ msgid "" msgstr "" "Language: en\n" -#: lib/mv_web/components/core_components.ex:386 +#: lib/mv_web/components/core_components.ex:378 #, elixir-autogen, elixir-format msgid "Actions" msgstr "Aktionen" -#: lib/mv_web/live/member_live/index.html.heex:227 +#: lib/mv_web/live/member_live/index.html.heex:202 #: lib/mv_web/live/user_live/index.html.heex:72 #, elixir-autogen, elixir-format msgid "Are you sure?" msgstr "Bist du sicher?" -#: lib/mv_web/components/layouts.ex:82 -#: lib/mv_web/components/layouts.ex:94 +#: lib/mv_web/components/layouts.ex:80 +#: lib/mv_web/components/layouts.ex:92 #, elixir-autogen, elixir-format 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/index.html.heex:148 #: lib/mv_web/live/member_live/show.ex:59 #, elixir-autogen, elixir-format msgid "City" msgstr "Stadt" -#: lib/mv_web/live/member_live/index.html.heex:229 +#: lib/mv_web/live/member_live/index.html.heex:204 #: lib/mv_web/live/user_live/index.html.heex:74 #, elixir-autogen, elixir-format msgid "Delete" msgstr "Löschen" -#: lib/mv_web/live/member_live/index.html.heex:221 +#: lib/mv_web/live/member_live/index.html.heex:196 #: lib/mv_web/live/user_live/form.ex:265 #: lib/mv_web/live/user_live/index.html.heex:66 #, elixir-autogen, elixir-format @@ -54,7 +54,7 @@ msgid "Edit Member" msgstr "Mitglied bearbeiten" #: 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:80 #: 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 @@ -70,7 +70,7 @@ 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/index.html.heex:182 #: lib/mv_web/live/member_live/show.ex:56 #, elixir-autogen, elixir-format msgid "Join Date" @@ -82,28 +82,28 @@ msgstr "Beitrittsdatum" msgid "Last Name" msgstr "Nachname" -#: lib/mv_web/live/member_live/index.html.heex:24 +#: lib/mv_web/live/member_live/index.html.heex:6 #, elixir-autogen, elixir-format 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:193 #: lib/mv_web/live/user_live/index.html.heex:63 #, elixir-autogen, elixir-format msgid "Show" msgstr "Anzeigen" -#: lib/mv_web/components/layouts.ex:89 +#: lib/mv_web/components/layouts.ex:87 #, elixir-autogen, elixir-format msgid "Something went wrong!" msgstr "Etwas ist schiefgelaufen!" -#: lib/mv_web/components/layouts.ex:77 +#: lib/mv_web/components/layouts.ex:75 #, elixir-autogen, elixir-format msgid "We can't find the internet" msgstr "Keine Internetverbindung gefunden" -#: lib/mv_web/components/core_components.ex:82 +#: lib/mv_web/components/core_components.ex:74 #, elixir-autogen, elixir-format msgid "close" msgstr "schließen" @@ -121,7 +121,7 @@ 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/index.html.heex:114 #: lib/mv_web/live/member_live/show.ex:61 #, elixir-autogen, elixir-format msgid "House Number" @@ -140,14 +140,14 @@ 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/index.html.heex:165 #: 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:57 -#: lib/mv_web/live/member_live/index.html.heex:153 +#: lib/mv_web/live/member_live/index.html.heex:131 #: lib/mv_web/live/member_live/show.ex:62 #, elixir-autogen, elixir-format msgid "Postal Code" @@ -168,7 +168,7 @@ 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/index.html.heex:97 #: lib/mv_web/live/member_live/show.ex:60 #, elixir-autogen, elixir-format msgid "Street" @@ -306,7 +306,7 @@ msgid "Member" msgstr "Mitglied" #: lib/mv_web/components/layouts/navbar.ex:25 -#: lib/mv_web/live/member_live/index.ex:73 +#: lib/mv_web/live/member_live/index.ex:57 #: lib/mv_web/live/member_live/index.html.heex:3 #, elixir-autogen, elixir-format msgid "Members" @@ -360,12 +360,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:37 #, 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:51 #, elixir-autogen, elixir-format msgid "Select member" msgstr "Mitglied auswählen" @@ -551,7 +551,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:15 #, elixir-autogen, elixir-format msgid "Search..." msgstr "Suchen..." @@ -567,7 +567,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:63 #, elixir-autogen, elixir-format msgid "First name" msgstr "Vorname" @@ -777,57 +777,25 @@ msgstr "Mitglied entverknüpfen" msgid "Unlinking scheduled" msgstr "Entverknüpfung geplant" -#: 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" -msgstr[0] "%{count} E-Mail-Adresse in die Zwischenablage kopiert" -msgstr[1] "%{count} E-Mail-Adressen in die Zwischenablage kopiert" - -#: lib/mv_web/live/member_live/index.html.heex:10 -#, elixir-autogen, elixir-format -msgid "Copy email addresses of selected members" -msgstr "E-Mail-Adressen der ausgewählten Mitglieder kopieren" - -#: lib/mv_web/live/member_live/index.html.heex:13 -#, elixir-autogen, elixir-format -msgid "Copy emails" -msgstr "E-Mails kopieren" - -#: 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:150 -#, elixir-autogen, elixir-format -msgid "No members selected" -msgstr "Keine Mitglieder ausgewählt" - -#: lib/mv_web/live/member_live/index.html.heex:18 -#, elixir-autogen, elixir-format -msgid "Open email program with BCC recipients" -msgstr "E-Mail-Programm mit BCC-Empfänger*innen öffnen" - -#: lib/mv_web/live/member_live/index.html.heex:21 -#, elixir-autogen, elixir-format -msgid "Open in email program" -msgstr "Im E-Mail-Programm öffnen" - -#: 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" - #: lib/mv_web/live/member_live/form.ex:40 #, elixir-autogen, elixir-format msgid "Fields marked with an asterisk (*) cannot be empty." msgstr "Felder, die mit einem Sternchen (*) markiert sind, dürfen nicht leer bleiben." -#: lib/mv_web/components/core_components.ex:206 -#: lib/mv_web/components/core_components.ex:223 -#: lib/mv_web/components/core_components.ex:250 -#: lib/mv_web/components/core_components.ex:277 +#: lib/mv_web/components/core_components.ex:198 +#: lib/mv_web/components/core_components.ex:215 +#: lib/mv_web/components/core_components.ex:242 +#: lib/mv_web/components/core_components.ex:269 #, elixir-autogen, elixir-format, fuzzy msgid "This field cannot be empty" msgstr "Dieses Feld darf nicht leer bleiben" + +#~ #: lib/mv_web/live/custom_field_live/index.ex:97 +#~ #, elixir-autogen, elixir-format +#~ msgid "To confirm deletion, please enter the custom field slug:" +#~ msgstr "Um die Löschung zu bestätigen, gib bitte den Slug des benutzerdefinierten Feldes ein:" + +#~ #: lib/mv_web/live/member_live/form.ex:40 +#~ #, elixir-autogen, elixir-format +#~ msgid "Use this form to manage member records and their properties." +#~ msgstr "Dieses Formular dient zur Verwaltung von Mitgliedern und deren Eigenschaften." diff --git a/priv/gettext/default.pot b/priv/gettext/default.pot index 3dd41b5..9acef50 100644 --- a/priv/gettext/default.pot +++ b/priv/gettext/default.pot @@ -11,37 +11,37 @@ msgid "" msgstr "" -#: lib/mv_web/components/core_components.ex:386 +#: lib/mv_web/components/core_components.ex:378 #, elixir-autogen, elixir-format msgid "Actions" msgstr "" -#: lib/mv_web/live/member_live/index.html.heex:227 +#: lib/mv_web/live/member_live/index.html.heex:202 #: lib/mv_web/live/user_live/index.html.heex:72 #, elixir-autogen, elixir-format msgid "Are you sure?" msgstr "" -#: lib/mv_web/components/layouts.ex:82 -#: lib/mv_web/components/layouts.ex:94 +#: lib/mv_web/components/layouts.ex:80 +#: lib/mv_web/components/layouts.ex:92 #, elixir-autogen, elixir-format 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/index.html.heex:148 #: lib/mv_web/live/member_live/show.ex:59 #, elixir-autogen, elixir-format msgid "City" msgstr "" -#: lib/mv_web/live/member_live/index.html.heex:229 +#: lib/mv_web/live/member_live/index.html.heex:204 #: lib/mv_web/live/user_live/index.html.heex:74 #, elixir-autogen, elixir-format msgid "Delete" msgstr "" -#: lib/mv_web/live/member_live/index.html.heex:221 +#: lib/mv_web/live/member_live/index.html.heex:196 #: lib/mv_web/live/user_live/form.ex:265 #: lib/mv_web/live/user_live/index.html.heex:66 #, elixir-autogen, elixir-format @@ -55,7 +55,7 @@ msgid "Edit Member" msgstr "" #: 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:80 #: 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 @@ -71,7 +71,7 @@ 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/index.html.heex:182 #: lib/mv_web/live/member_live/show.ex:56 #, elixir-autogen, elixir-format msgid "Join Date" @@ -83,28 +83,28 @@ msgstr "" msgid "Last Name" msgstr "" -#: lib/mv_web/live/member_live/index.html.heex:24 +#: lib/mv_web/live/member_live/index.html.heex:6 #, elixir-autogen, elixir-format msgid "New Member" msgstr "" -#: lib/mv_web/live/member_live/index.html.heex:218 +#: lib/mv_web/live/member_live/index.html.heex:193 #: lib/mv_web/live/user_live/index.html.heex:63 #, elixir-autogen, elixir-format msgid "Show" msgstr "" -#: lib/mv_web/components/layouts.ex:89 +#: lib/mv_web/components/layouts.ex:87 #, elixir-autogen, elixir-format msgid "Something went wrong!" msgstr "" -#: lib/mv_web/components/layouts.ex:77 +#: lib/mv_web/components/layouts.ex:75 #, elixir-autogen, elixir-format msgid "We can't find the internet" msgstr "" -#: lib/mv_web/components/core_components.ex:82 +#: lib/mv_web/components/core_components.ex:74 #, elixir-autogen, elixir-format msgid "close" msgstr "" @@ -122,7 +122,7 @@ 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/index.html.heex:114 #: lib/mv_web/live/member_live/show.ex:61 #, elixir-autogen, elixir-format msgid "House Number" @@ -141,14 +141,14 @@ 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/index.html.heex:165 #: 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:57 -#: lib/mv_web/live/member_live/index.html.heex:153 +#: lib/mv_web/live/member_live/index.html.heex:131 #: lib/mv_web/live/member_live/show.ex:62 #, elixir-autogen, elixir-format msgid "Postal Code" @@ -169,7 +169,7 @@ 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/index.html.heex:97 #: lib/mv_web/live/member_live/show.ex:60 #, elixir-autogen, elixir-format msgid "Street" @@ -307,7 +307,7 @@ msgid "Member" msgstr "" #: lib/mv_web/components/layouts/navbar.ex:25 -#: lib/mv_web/live/member_live/index.ex:73 +#: lib/mv_web/live/member_live/index.ex:57 #: lib/mv_web/live/member_live/index.html.heex:3 #, elixir-autogen, elixir-format msgid "Members" @@ -361,12 +361,12 @@ msgstr "" msgid "Required" msgstr "" -#: lib/mv_web/live/member_live/index.html.heex:55 +#: lib/mv_web/live/member_live/index.html.heex:37 #, 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:51 #, elixir-autogen, elixir-format msgid "Select member" msgstr "" @@ -552,7 +552,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:15 #, elixir-autogen, elixir-format msgid "Search..." msgstr "" @@ -568,7 +568,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:63 #, elixir-autogen, elixir-format msgid "First name" msgstr "" @@ -778,57 +778,15 @@ msgstr "" msgid "Unlinking scheduled" msgstr "" -#: 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" -msgstr[0] "" -msgstr[1] "" - -#: lib/mv_web/live/member_live/index.html.heex:10 -#, elixir-autogen, elixir-format -msgid "Copy email addresses of selected members" -msgstr "" - -#: lib/mv_web/live/member_live/index.html.heex:13 -#, elixir-autogen, elixir-format -msgid "Copy emails" -msgstr "" - -#: 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:150 -#, elixir-autogen, elixir-format -msgid "No members selected" -msgstr "" - -#: lib/mv_web/live/member_live/index.html.heex:18 -#, elixir-autogen, elixir-format -msgid "Open email program with BCC recipients" -msgstr "" - -#: lib/mv_web/live/member_live/index.html.heex:21 -#, elixir-autogen, elixir-format -msgid "Open in email program" -msgstr "" - -#: 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 "" - #: lib/mv_web/live/member_live/form.ex:40 #, elixir-autogen, elixir-format msgid "Fields marked with an asterisk (*) cannot be empty." msgstr "" -#: lib/mv_web/components/core_components.ex:206 -#: lib/mv_web/components/core_components.ex:223 -#: lib/mv_web/components/core_components.ex:250 -#: lib/mv_web/components/core_components.ex:277 +#: lib/mv_web/components/core_components.ex:198 +#: lib/mv_web/components/core_components.ex:215 +#: lib/mv_web/components/core_components.ex:242 +#: lib/mv_web/components/core_components.ex:269 #, elixir-autogen, elixir-format msgid "This field cannot be empty" msgstr "" diff --git a/priv/gettext/en/LC_MESSAGES/default.po b/priv/gettext/en/LC_MESSAGES/default.po index 125ce4e..4b9bc09 100644 --- a/priv/gettext/en/LC_MESSAGES/default.po +++ b/priv/gettext/en/LC_MESSAGES/default.po @@ -11,37 +11,37 @@ msgstr "" "Language: en\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" -#: lib/mv_web/components/core_components.ex:386 +#: lib/mv_web/components/core_components.ex:378 #, elixir-autogen, elixir-format msgid "Actions" msgstr "" -#: lib/mv_web/live/member_live/index.html.heex:227 +#: lib/mv_web/live/member_live/index.html.heex:202 #: lib/mv_web/live/user_live/index.html.heex:72 #, elixir-autogen, elixir-format msgid "Are you sure?" msgstr "" -#: lib/mv_web/components/layouts.ex:82 -#: lib/mv_web/components/layouts.ex:94 +#: lib/mv_web/components/layouts.ex:80 +#: lib/mv_web/components/layouts.ex:92 #, elixir-autogen, elixir-format 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/index.html.heex:148 #: lib/mv_web/live/member_live/show.ex:59 #, elixir-autogen, elixir-format msgid "City" msgstr "" -#: lib/mv_web/live/member_live/index.html.heex:229 +#: lib/mv_web/live/member_live/index.html.heex:204 #: lib/mv_web/live/user_live/index.html.heex:74 #, elixir-autogen, elixir-format msgid "Delete" msgstr "" -#: lib/mv_web/live/member_live/index.html.heex:221 +#: lib/mv_web/live/member_live/index.html.heex:196 #: lib/mv_web/live/user_live/form.ex:265 #: lib/mv_web/live/user_live/index.html.heex:66 #, elixir-autogen, elixir-format @@ -55,7 +55,7 @@ msgid "Edit Member" msgstr "" #: 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:80 #: 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 @@ -71,7 +71,7 @@ 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/index.html.heex:182 #: lib/mv_web/live/member_live/show.ex:56 #, elixir-autogen, elixir-format msgid "Join Date" @@ -83,28 +83,28 @@ msgstr "" msgid "Last Name" msgstr "" -#: lib/mv_web/live/member_live/index.html.heex:24 +#: lib/mv_web/live/member_live/index.html.heex:6 #, elixir-autogen, elixir-format msgid "New Member" msgstr "" -#: lib/mv_web/live/member_live/index.html.heex:218 +#: lib/mv_web/live/member_live/index.html.heex:193 #: lib/mv_web/live/user_live/index.html.heex:63 #, elixir-autogen, elixir-format msgid "Show" msgstr "" -#: lib/mv_web/components/layouts.ex:89 +#: lib/mv_web/components/layouts.ex:87 #, elixir-autogen, elixir-format msgid "Something went wrong!" msgstr "" -#: lib/mv_web/components/layouts.ex:77 +#: lib/mv_web/components/layouts.ex:75 #, elixir-autogen, elixir-format msgid "We can't find the internet" msgstr "" -#: lib/mv_web/components/core_components.ex:82 +#: lib/mv_web/components/core_components.ex:74 #, elixir-autogen, elixir-format msgid "close" msgstr "" @@ -122,7 +122,7 @@ 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/index.html.heex:114 #: lib/mv_web/live/member_live/show.ex:61 #, elixir-autogen, elixir-format msgid "House Number" @@ -141,14 +141,14 @@ 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/index.html.heex:165 #: 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:57 -#: lib/mv_web/live/member_live/index.html.heex:153 +#: lib/mv_web/live/member_live/index.html.heex:131 #: lib/mv_web/live/member_live/show.ex:62 #, elixir-autogen, elixir-format msgid "Postal Code" @@ -169,7 +169,7 @@ 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/index.html.heex:97 #: lib/mv_web/live/member_live/show.ex:60 #, elixir-autogen, elixir-format msgid "Street" @@ -307,7 +307,7 @@ msgid "Member" msgstr "" #: lib/mv_web/components/layouts/navbar.ex:25 -#: lib/mv_web/live/member_live/index.ex:73 +#: lib/mv_web/live/member_live/index.ex:57 #: lib/mv_web/live/member_live/index.html.heex:3 #, elixir-autogen, elixir-format msgid "Members" @@ -361,12 +361,12 @@ msgstr "" msgid "Required" msgstr "" -#: lib/mv_web/live/member_live/index.html.heex:55 +#: lib/mv_web/live/member_live/index.html.heex:37 #, 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:51 #, elixir-autogen, elixir-format msgid "Select member" msgstr "" @@ -552,7 +552,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:15 #, elixir-autogen, elixir-format msgid "Search..." msgstr "" @@ -568,7 +568,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:63 #, elixir-autogen, elixir-format, fuzzy msgid "First name" msgstr "" @@ -778,57 +778,25 @@ msgstr "" msgid "Unlinking scheduled" msgstr "" -#: 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" -msgstr[0] "" -msgstr[1] "" - -#: lib/mv_web/live/member_live/index.html.heex:10 -#, elixir-autogen, elixir-format -msgid "Copy email addresses of selected members" -msgstr "" - -#: lib/mv_web/live/member_live/index.html.heex:13 -#, elixir-autogen, elixir-format -msgid "Copy emails" -msgstr "" - -#: 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:150 -#, elixir-autogen, elixir-format, fuzzy -msgid "No members selected" -msgstr "" - -#: lib/mv_web/live/member_live/index.html.heex:18 -#, elixir-autogen, elixir-format -msgid "Open email program with BCC recipients" -msgstr "" - -#: lib/mv_web/live/member_live/index.html.heex:21 -#, elixir-autogen, elixir-format -msgid "Open in email program" -msgstr "" - -#: 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 "" - #: lib/mv_web/live/member_live/form.ex:40 #, elixir-autogen, elixir-format msgid "Fields marked with an asterisk (*) cannot be empty." msgstr "" -#: lib/mv_web/components/core_components.ex:206 -#: lib/mv_web/components/core_components.ex:223 -#: lib/mv_web/components/core_components.ex:250 -#: lib/mv_web/components/core_components.ex:277 +#: lib/mv_web/components/core_components.ex:198 +#: lib/mv_web/components/core_components.ex:215 +#: lib/mv_web/components/core_components.ex:242 +#: lib/mv_web/components/core_components.ex:269 #, elixir-autogen, elixir-format, fuzzy msgid "This field cannot be empty" msgstr "" + +#~ #: lib/mv_web/live/custom_field_live/index.ex:97 +#~ #, elixir-autogen, elixir-format +#~ msgid "To confirm deletion, please enter the custom field slug:" +#~ msgstr "" + +#~ #: lib/mv_web/live/member_live/form.ex:40 +#~ #, elixir-autogen, elixir-format +#~ msgid "Use this form to manage member records and their properties." +#~ msgstr "" diff --git a/priv/repo/migrations/20251201115939_add_member_field_visibility_to_settings.exs b/priv/repo/migrations/20251201115939_add_member_field_visibility_to_settings.exs deleted file mode 100644 index 6d278fb..0000000 --- a/priv/repo/migrations/20251201115939_add_member_field_visibility_to_settings.exs +++ /dev/null @@ -1,21 +0,0 @@ -defmodule Mv.Repo.Migrations.AddMemberFieldVisibilityToSettings do - @moduledoc """ - Updates resources based on their most recent snapshots. - - This file was autogenerated with `mix ash_postgres.generate_migrations` - """ - - use Ecto.Migration - - def up do - alter table(:settings) do - add :member_field_visibility, :map - end - end - - def down do - alter table(:settings) do - remove :member_field_visibility - end - end -end diff --git a/priv/resource_snapshots/repo/custom_fields/20251201115939.json b/priv/resource_snapshots/repo/custom_fields/20251201115939.json deleted file mode 100644 index fabd84b..0000000 --- a/priv/resource_snapshots/repo/custom_fields/20251201115939.json +++ /dev/null @@ -1,144 +0,0 @@ -{ - "attributes": [ - { - "allow_nil?": false, - "default": "fragment(\"gen_random_uuid()\")", - "generated?": false, - "precision": null, - "primary_key?": true, - "references": null, - "scale": null, - "size": null, - "source": "id", - "type": "uuid" - }, - { - "allow_nil?": false, - "default": "nil", - "generated?": false, - "precision": null, - "primary_key?": false, - "references": null, - "scale": null, - "size": null, - "source": "name", - "type": "text" - }, - { - "allow_nil?": false, - "default": "nil", - "generated?": false, - "precision": null, - "primary_key?": false, - "references": null, - "scale": null, - "size": null, - "source": "slug", - "type": "text" - }, - { - "allow_nil?": false, - "default": "nil", - "generated?": false, - "precision": null, - "primary_key?": false, - "references": null, - "scale": null, - "size": null, - "source": "value_type", - "type": "text" - }, - { - "allow_nil?": true, - "default": "nil", - "generated?": false, - "precision": null, - "primary_key?": false, - "references": null, - "scale": null, - "size": null, - "source": "description", - "type": "text" - }, - { - "allow_nil?": false, - "default": "false", - "generated?": false, - "precision": null, - "primary_key?": false, - "references": null, - "scale": null, - "size": null, - "source": "immutable", - "type": "boolean" - }, - { - "allow_nil?": false, - "default": "false", - "generated?": false, - "precision": null, - "primary_key?": false, - "references": null, - "scale": null, - "size": null, - "source": "required", - "type": "boolean" - }, - { - "allow_nil?": false, - "default": "true", - "generated?": false, - "precision": null, - "primary_key?": false, - "references": null, - "scale": null, - "size": null, - "source": "show_in_overview", - "type": "boolean" - } - ], - "base_filter": null, - "check_constraints": [], - "custom_indexes": [], - "custom_statements": [], - "has_create_action": true, - "hash": "D31160C95D3D32BA715D493DE2D2B8D6572E0EC68AE14B928D99975BC8A81542", - "identities": [ - { - "all_tenants?": false, - "base_filter": null, - "index_name": "custom_fields_unique_name_index", - "keys": [ - { - "type": "atom", - "value": "name" - } - ], - "name": "unique_name", - "nils_distinct?": true, - "where": null - }, - { - "all_tenants?": false, - "base_filter": null, - "index_name": "custom_fields_unique_slug_index", - "keys": [ - { - "type": "atom", - "value": "slug" - } - ], - "name": "unique_slug", - "nils_distinct?": true, - "where": null - } - ], - "multitenancy": { - "attribute": null, - "global": null, - "strategy": null - }, - "repo": "Elixir.Mv.Repo", - "schema": null, - "table": "custom_fields" -} \ No newline at end of file diff --git a/priv/resource_snapshots/repo/settings/20251201115939.json b/priv/resource_snapshots/repo/settings/20251201115939.json deleted file mode 100644 index 4e635c4..0000000 --- a/priv/resource_snapshots/repo/settings/20251201115939.json +++ /dev/null @@ -1,79 +0,0 @@ -{ - "attributes": [ - { - "allow_nil?": false, - "default": "fragment(\"gen_random_uuid()\")", - "generated?": false, - "precision": null, - "primary_key?": true, - "references": null, - "scale": null, - "size": null, - "source": "id", - "type": "uuid" - }, - { - "allow_nil?": false, - "default": "nil", - "generated?": false, - "precision": null, - "primary_key?": false, - "references": null, - "scale": null, - "size": null, - "source": "club_name", - "type": "text" - }, - { - "allow_nil?": true, - "default": "nil", - "generated?": false, - "precision": null, - "primary_key?": false, - "references": null, - "scale": null, - "size": null, - "source": "member_field_visibility", - "type": "map" - }, - { - "allow_nil?": false, - "default": "fragment(\"(now() AT TIME ZONE 'utc')\")", - "generated?": false, - "precision": null, - "primary_key?": false, - "references": null, - "scale": null, - "size": null, - "source": "inserted_at", - "type": "utc_datetime_usec" - }, - { - "allow_nil?": false, - "default": "fragment(\"(now() AT TIME ZONE 'utc')\")", - "generated?": false, - "precision": null, - "primary_key?": false, - "references": null, - "scale": null, - "size": null, - "source": "updated_at", - "type": "utc_datetime_usec" - } - ], - "base_filter": null, - "check_constraints": [], - "custom_indexes": [], - "custom_statements": [], - "has_create_action": true, - "hash": "F2823210AA9E6476074A218375F64CD80E7F9E04EECC4E94D4C7FD31A773C016", - "identities": [], - "multitenancy": { - "attribute": null, - "global": null, - "strategy": null - }, - "repo": "Elixir.Mv.Repo", - "schema": null, - "table": "settings" -} \ No newline at end of file diff --git a/test/membership/member_field_visibility_test.exs b/test/membership/member_field_visibility_test.exs deleted file mode 100644 index 9963169..0000000 --- a/test/membership/member_field_visibility_test.exs +++ /dev/null @@ -1,14 +0,0 @@ -defmodule Mv.Membership.MemberFieldVisibilityTest do - @moduledoc """ - Tests for member field visibility configuration. - - Tests cover: - - Member fields are visible by default (show_in_overview: true) - - Member fields can be hidden (show_in_overview: false) - - Checking if a specific field is visible - - Configuration is stored in Settings resource - """ - use Mv.DataCase, async: true - - alias Mv.Membership.Member -end diff --git a/test/mv_web/member_live/index_member_fields_display_test.exs b/test/mv_web/member_live/index_member_fields_display_test.exs deleted file mode 100644 index 6b4f50c..0000000 --- a/test/mv_web/member_live/index_member_fields_display_test.exs +++ /dev/null @@ -1,64 +0,0 @@ -defmodule MvWeb.MemberLive.IndexMemberFieldsDisplayTest do - use MvWeb.ConnCase, async: true - import Phoenix.LiveViewTest - require Ash.Query - - alias Mv.Membership.Member - - setup do - {:ok, member1} = - Member - |> Ash.Changeset.for_create(:create_member, %{ - first_name: "Alice", - last_name: "Anderson", - email: "alice@example.com", - street: "Main Street", - house_number: "123", - postal_code: "12345", - city: "Berlin", - phone_number: "+49123456789", - join_date: ~D[2020-01-15] - }) - |> Ash.create() - - {:ok, member2} = - Member - |> Ash.Changeset.for_create(:create_member, %{ - first_name: "Bob", - last_name: "Brown", - email: "bob@example.com" - }) - |> Ash.create() - - %{ - member1: member1, - member2: member2 - } - end - - test "shows multiple members correctly", %{conn: conn, member1: m1, member2: m2} do - conn = conn_with_oidc_user(conn) - {:ok, _view, html} = live(conn, "/members") - - for m <- [m1, m2], field <- [m.first_name, m.last_name, m.email] do - assert html =~ field - end - end - - test "respects show_in_overview config", %{conn: conn, member1: m} do - {:ok, settings} = Mv.Membership.get_settings() - fields_to_hide = [:street, :house_number] - - {:ok, _} = - Mv.Membership.update_settings(settings, %{ - member_field_visibility: Map.new(fields_to_hide, &{Atom.to_string(&1), false}) - }) - - conn = conn_with_oidc_user(conn) - {:ok, _view, html} = live(conn, "/members") - - assert html =~ "Email" - assert html =~ m.email - refute html =~ m.street - end -end diff --git a/test/mv_web/member_live/index_test.exs b/test/mv_web/member_live/index_test.exs index e3ad5bb..0668202 100644 --- a/test/mv_web/member_live/index_test.exs +++ b/test/mv_web/member_live/index_test.exs @@ -249,224 +249,4 @@ defmodule MvWeb.MemberLive.IndexTest do # Verify the member was actually deleted from the database assert not (Mv.Membership.Member |> Ash.Query.filter(id == ^member.id) |> Ash.exists?()) end - - describe "copy_emails feature" do - setup do - # Create test members - {:ok, member1} = - Mv.Membership.create_member(%{ - first_name: "Max", - last_name: "Mustermann", - email: "max@example.com" - }) - - {:ok, member2} = - Mv.Membership.create_member(%{ - first_name: "Erika", - last_name: "Musterfrau", - email: "erika@example.com" - }) - - {:ok, member3} = - Mv.Membership.create_member(%{ - first_name: "Hans", - last_name: "Müller-Lüdenscheidt", - email: "hans@example.com" - }) - - %{member1: member1, member2: member2, member3: member3} - end - - test "copy_emails event formats selected members correctly", %{ - conn: conn, - member1: member1, - member2: member2 - } do - conn = conn_with_oidc_user(conn) - {:ok, view, _html} = live(conn, "/members") - - # Select two members - view - |> element("[phx-click='select_member'][phx-value-id='#{member1.id}']") - |> render_click() - - view - |> element("[phx-click='select_member'][phx-value-id='#{member2.id}']") - |> render_click() - - # Trigger copy_emails event - view |> element("#copy-emails-btn") |> render_click() - - # Verify flash message shows correct count - assert render(view) =~ "2" - end - - test "copy_emails event with no selection shows error flash", %{conn: conn} do - conn = conn_with_oidc_user(conn) - {:ok, view, _html} = live(conn, "/members") - - # Trigger copy_emails event directly (button not visible when no selection) - # This tests the edge case where event is triggered without selection - result = render_hook(view, "copy_emails", %{}) - - # Should show error flash - assert result =~ "No members selected" or result =~ "Keine Mitglieder" - end - - test "copy_emails event with all members selected formats all emails", %{ - conn: conn - } do - conn = conn_with_oidc_user(conn) - {:ok, view, _html} = live(conn, "/members") - - # Select all members via select_all - view |> element("[phx-click='select_all']") |> render_click() - - # Trigger copy_emails event - view |> element("#copy-emails-btn") |> render_click() - - # Verify flash message shows correct count (3 members) - assert render(view) =~ "3" - end - - test "copy_emails handles members with special characters in names", %{ - conn: conn, - member3: member3 - } do - conn = conn_with_oidc_user(conn) - {:ok, view, _html} = live(conn, "/members") - - # Select member with umlauts - view - |> element("[phx-click='select_member'][phx-value-id='#{member3.id}']") - |> render_click() - - # Trigger copy_emails event - should not crash - view |> element("#copy-emails-btn") |> render_click() - - # Verify flash message shows success - assert render(view) =~ "1" - end - - test "copy_emails handles case where selected member is deleted before copy", %{ - conn: conn, - member1: member1 - } do - conn = conn_with_oidc_user(conn) - {:ok, view, _html} = live(conn, "/members") - - # Select a member - view - |> element("[phx-click='select_member'][phx-value-id='#{member1.id}']") - |> render_click() - - # Delete the member from the database - Ash.destroy!(member1) - - # Trigger copy_emails event directly - selection still contains the deleted ID - # but the member is no longer in @members list after reload - result = render_hook(view, "copy_emails", %{}) - - # Should show error since no visible members match selection - assert result =~ "No email" or result =~ "Keine E-Mail" or result =~ "0" - end - - test "copy_emails formats emails as RFC 5322 compliant comma-separated list", %{ - conn: conn, - member1: member1, - member2: member2 - } do - conn = conn_with_oidc_user(conn) - {:ok, view, _html} = live(conn, "/members") - - # Select two members - view - |> element("[phx-click='select_member'][phx-value-id='#{member1.id}']") - |> render_click() - - view - |> element("[phx-click='select_member'][phx-value-id='#{member2.id}']") - |> render_click() - - # Get the socket state to verify the formatted email string - state = :sys.get_state(view.pid) - selected_members = state.socket.assigns.selected_members - - # Verify MapSet is used - assert %MapSet{} = selected_members - assert MapSet.size(selected_members) == 2 - end - - test "email format is 'First Last ' with comma separator", %{ - conn: conn, - member1: _member1 - } do - # Test the format_member_email function indirectly - # by checking the push_event payload structure - conn = conn_with_oidc_user(conn) - - # Create a member with known data - {:ok, test_member} = - Mv.Membership.create_member(%{ - first_name: "Test", - last_name: "Format", - email: "test.format@example.com" - }) - - {:ok, view, _html} = live(conn, "/members") - - # Select the test member - view - |> element("[phx-click='select_member'][phx-value-id='#{test_member.id}']") - |> render_click() - - # The format should be "Test Format " - # We verify this by checking the flash shows 1 email was copied - view |> element("#copy-emails-btn") |> render_click() - assert render(view) =~ "1" - end - - test "copy button is not visible when no members are selected", %{conn: conn} do - conn = conn_with_oidc_user(conn) - {:ok, view, _html} = live(conn, "/members") - - # Ensure no members are selected (default state) - refute has_element?(view, "#copy-emails-btn") - end - - test "copy button is visible when members are selected", %{ - conn: conn, - member1: member1 - } do - conn = conn_with_oidc_user(conn) - {:ok, view, _html} = live(conn, "/members") - - # Select a member - view - |> element("[phx-click='select_member'][phx-value-id='#{member1.id}']") - |> render_click() - - # Button should now be visible - assert has_element?(view, "#copy-emails-btn") - end - - test "copy button click triggers event and shows flash", %{ - conn: conn, - member1: member1 - } do - conn = conn_with_oidc_user(conn) - {:ok, view, _html} = live(conn, "/members") - - # Select a member - view - |> element("[phx-click='select_member'][phx-value-id='#{member1.id}']") - |> render_click() - - # Click copy button - view |> element("#copy-emails-btn") |> render_click() - - # Flash message should appear - assert has_element?(view, "#flash-group") - end - end end