diff --git a/lib/accounts/user.ex b/lib/accounts/user.ex
index b085407..668ddd4 100644
--- a/lib/accounts/user.ex
+++ b/lib/accounts/user.ex
@@ -12,6 +12,12 @@ defmodule Mv.Accounts.User do
postgres do
table "users"
repo Mv.Repo
+
+ references do
+ # When a member is deleted, set the user's member_id to NULL
+ # This allows users to continue existing even if their linked member is removed
+ reference :member, on_delete: :nilify
+ end
end
@doc """
@@ -60,15 +66,77 @@ defmodule Mv.Accounts.User do
end
actions do
- defaults [:read, :create, :destroy, :update]
+ # Default actions kept for framework/tooling integration:
+ # - :create -> Used by AshAdmin's generated "Create" UI and by generic
+ # AshPhoenix helpers that assume a default create action.
+ # It does NOT manage the :member relationship. For admin
+ # flows that may link an existing member, use :create_user.
+ # - :read -> Standard read used across the app and by admin tooling.
+ # - :destroy-> Standard delete used by admin tooling and maintenance tasks.
+ defaults [:read, :create, :destroy]
+
+ # Primary generic update action:
+ # - Selected by AshAdmin's generated "Edit" UI and generic AshPhoenix
+ # helpers that assume a default update action.
+ # - Intended for simple attribute updates (e.g., :email) and scenarios
+ # that do NOT need to manage the :member relationship.
+ # - For linking/unlinking a member (and the related validations), prefer
+ # the specialized :update_user action below.
+ update :update do
+ primary? true
+
+ # Required because custom validation functions (email validation, member relationship validation)
+ # cannot be executed atomically. These validations need to query the database and perform
+ # complex checks that are not supported in atomic operations.
+ require_atomic? false
+ end
create :create_user do
+ description "Creates a new user with optional member relationship. The member relationship is managed through the :member argument."
+ # Only accept email directly - member_id is NOT in accept list
+ # This prevents direct foreign key manipulation, forcing use of manage_relationship
accept [:email]
+ # Allow member to be passed as argument for relationship management
+ argument :member, :map, allow_nil?: true
upsert? true
+
+ # Manage the member relationship during user creation
+ change manage_relationship(:member, :member,
+ # Look up existing member and relate to it
+ on_lookup: :relate,
+ # Error if member doesn't exist in database
+ on_no_match: :error,
+ # If member already linked to this user, ignore (shouldn't happen in create)
+ on_match: :ignore,
+ # If no member provided, that's fine (optional relationship)
+ on_missing: :ignore
+ )
end
update :update_user do
+ description "Updates a user and manages the optional member relationship. To change an existing member link, first remove it (set member to nil), then add the new one."
+ # Only accept email directly - member_id is NOT in accept list
+ # This prevents direct foreign key manipulation, forcing use of manage_relationship
accept [:email]
+ # Allow member to be passed as argument for relationship management
+ argument :member, :map, allow_nil?: true
+
+ # Required because custom validation functions (email validation, member relationship validation)
+ # cannot be executed atomically. These validations need to query the database and perform
+ # complex checks that are not supported in atomic operations.
+ require_atomic? false
+
+ # Manage the member relationship during user update
+ change manage_relationship(:member, :member,
+ # Look up existing member and relate to it
+ on_lookup: :relate,
+ # Error if member doesn't exist in database
+ on_no_match: :error,
+ # If same member provided, that's fine (allows updates with same member)
+ on_match: :ignore,
+ # If no member provided, remove existing relationship (allows member removal)
+ on_missing: :unrelate
+ )
end
# Admin action for direct password changes in admin panel
@@ -76,6 +144,7 @@ defmodule Mv.Accounts.User do
update :admin_set_password do
accept [:email]
argument :password, :string, allow_nil?: false, sensitive?: true
+ require_atomic? false
# Set the strategy context that HashPasswordChange expects
change set_context(%{strategy_name: :password})
@@ -122,8 +191,54 @@ defmodule Mv.Accounts.User do
# Global validations - applied to all relevant actions
validations do
# Password strength policy: minimum 8 characters for all password-related actions
- validate string_length(:password, min: 8) do
- where action_is([:register_with_password, :admin_set_password])
+ validate string_length(:password, min: 8),
+ where: [action_is([:register_with_password, :admin_set_password])],
+ message: "must have length of at least 8"
+
+ # Email validation with EctoCommons.EmailValidator (same as Member)
+ # This ensures consistency between User and Member email validation
+ validate fn changeset, _ ->
+ # Get email from attribute (Ash.CiString) and convert to string
+ email = Ash.Changeset.get_attribute(changeset, :email)
+ email_string = if email, do: to_string(email), else: nil
+
+ # Only validate if email is present
+ if email_string do
+ changeset2 =
+ {%{}, %{email: :string}}
+ |> Ecto.Changeset.cast(%{email: email_string}, [:email])
+ |> EctoCommons.EmailValidator.validate_email(:email, checks: [:html_input, :pow])
+
+ if changeset2.valid? do
+ :ok
+ else
+ {:error, field: :email, message: "is not a valid email"}
+ end
+ else
+ :ok
+ end
+ end
+
+ # Prevent overwriting existing member relationship
+ # This validation ensures race condition safety by requiring explicit two-step process:
+ # 1. Remove existing member (set member to nil)
+ # 2. Add new member
+ # This prevents accidental overwrites when multiple admins work simultaneously
+ validate fn changeset, _context ->
+ member_arg = Ash.Changeset.get_argument(changeset, :member)
+ current_member_id = changeset.data.member_id
+
+ # Only trigger if:
+ # - member argument is provided AND has an ID
+ # - user currently has a member
+ # - the new member ID is different from current member ID
+ if member_arg && member_arg[:id] && current_member_id &&
+ member_arg[:id] != current_member_id do
+ {:error,
+ field: :member, message: "User already has a member. Remove existing member first."}
+ else
+ :ok
+ end
end
end
@@ -140,18 +255,28 @@ defmodule Mv.Accounts.User do
attributes do
uuid_primary_key :id
- attribute :email, :ci_string, allow_nil?: false, public?: true
+ attribute :email, :ci_string do
+ allow_nil? false
+ public? true
+ # Same constraints as Member email for consistency
+ constraints min_length: 5, max_length: 254
+ end
+
attribute :hashed_password, :string, sensitive?: true, allow_nil?: true
attribute :oidc_id, :string, allow_nil?: true
end
relationships do
+ # 1:1 relationship - User can optionally belong to one Member
+ # This automatically creates a `member_id` attribute in the User table
+ # The relationship is optional (allow_nil? true by default)
belongs_to :member, Mv.Membership.Member
end
identities do
identity :unique_email, [:email]
identity :unique_oidc_id, [:oidc_id]
+ identity :unique_member, [:member_id]
end
# You can customize this if you wish, but this is a safe default that
diff --git a/lib/membership/member.ex b/lib/membership/member.ex
index 7fe69da..4cec072 100644
--- a/lib/membership/member.ex
+++ b/lib/membership/member.ex
@@ -13,7 +13,11 @@ defmodule Mv.Membership.Member do
create :create_member do
primary? true
+ # Properties can be created along with member
argument :properties, {:array, :map}
+ # Allow user to be passed as argument for relationship management
+ # user_id is NOT in accept list to prevent direct foreign key manipulation
+ argument :user, :map, allow_nil?: true
accept [
:first_name,
@@ -32,12 +36,29 @@ defmodule Mv.Membership.Member do
]
change manage_relationship(:properties, type: :create)
+
+ # Manage the user relationship during member creation
+ change manage_relationship(:user, :user,
+ # Look up existing user and relate to it
+ on_lookup: :relate,
+ # Error if user doesn't exist in database
+ on_no_match: :error,
+ # Error if user is already linked to another member (prevents "stealing")
+ on_match: :error,
+ # If no user provided, that's fine (optional relationship)
+ on_missing: :ignore
+ )
end
update :update_member do
primary? true
+ # Required because custom validation function cannot be done atomically
require_atomic? false
+ # Properties can be updated or created along with member
argument :properties, {:array, :map}
+ # Allow user to be passed as argument for relationship management
+ # user_id is NOT in accept list to prevent direct foreign key manipulation
+ argument :user, :map, allow_nil?: true
accept [
:first_name,
@@ -56,6 +77,18 @@ defmodule Mv.Membership.Member do
]
change manage_relationship(:properties, on_match: :update, on_no_match: :create)
+
+ # Manage the user relationship during member update
+ change manage_relationship(:user, :user,
+ # Look up existing user and relate to it
+ on_lookup: :relate,
+ # Error if user doesn't exist in database
+ on_no_match: :error,
+ # Error if user is already linked to another member (prevents "stealing")
+ on_match: :error,
+ # If no user provided, remove existing relationship (allows user removal)
+ on_missing: :unrelate
+ )
end
end
@@ -67,6 +100,40 @@ defmodule Mv.Membership.Member do
validate present(:last_name)
validate present(:email)
+ # Prevent linking to a user that already has a member
+ # This validation prevents "stealing" users from other members by checking
+ # if the target user is already linked to a different member
+ # This is necessary because manage_relationship's on_match: :error only checks
+ # if the user is already linked to THIS specific member, not ANY member
+ validate fn changeset, _context ->
+ user_arg = Ash.Changeset.get_argument(changeset, :user)
+
+ if user_arg && user_arg[:id] do
+ user_id = user_arg[:id]
+ current_member_id = changeset.data.id
+
+ # Check the current state of the user in the database
+ case Ash.get(Mv.Accounts.User, user_id) do
+ # User is free to be linked
+ {:ok, %{member_id: nil}} ->
+ :ok
+
+ # User already linked to this member (update scenario)
+ {:ok, %{member_id: ^current_member_id}} ->
+ :ok
+
+ {:ok, %{member_id: _other_member_id}} ->
+ # User is linked to a different member - prevent "stealing"
+ {:error, field: :user, message: "User is already linked to another member"}
+
+ {:error, _} ->
+ {:error, field: :user, message: "User not found"}
+ end
+ else
+ :ok
+ 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)],
@@ -175,5 +242,14 @@ defmodule Mv.Membership.Member do
relationships do
has_many :properties, Mv.Membership.Property
+ # 1:1 relationship - Member can optionally have one User
+ # This references the User's member_id attribute
+ # The relationship is optional (allow_nil? true by default)
+ has_one :user, Mv.Accounts.User
+ end
+
+ # Define identities for upsert operations
+ identities do
+ identity :unique_email, [:email]
end
end
diff --git a/lib/membership/property.ex b/lib/membership/property.ex
index 2c432a8..de096ca 100644
--- a/lib/membership/property.ex
+++ b/lib/membership/property.ex
@@ -42,4 +42,10 @@ defmodule Mv.Membership.Property do
calculations do
calculate :value_to_string, :string, expr(value[:value] <> "")
end
+
+ # Ensure a member can only have one property per property type
+ # For example: A member can have only one "email" property, one "phone" property, etc.
+ identities do
+ identity :unique_property_per_member, [:member_id, :property_type_id]
+ end
end
diff --git a/lib/mv_web/components/layouts/navbar.ex b/lib/mv_web/components/layouts/navbar.ex
index 9009329..9fec3f4 100644
--- a/lib/mv_web/components/layouts/navbar.ex
+++ b/lib/mv_web/components/layouts/navbar.ex
@@ -16,20 +16,27 @@ defmodule MvWeb.Layouts.Navbar do
-
+
-
+
diff --git a/lib/mv_web/live/member_live/show.ex b/lib/mv_web/live/member_live/show.ex
index fbf5b4a..9a0ef40 100644
--- a/lib/mv_web/live/member_live/show.ex
+++ b/lib/mv_web/live/member_live/show.ex
@@ -11,8 +11,9 @@ defmodule MvWeb.MemberLive.Show do
<:subtitle>{gettext("This is a member record from your database.")}
<:actions>
- <.button navigate={~p"/members"}>
+ <.button navigate={~p"/members"} aria-label={gettext("Back to members list")}>
<.icon name="hero-arrow-left" />
+ {gettext("Back to members list")}
<.button variant="primary" navigate={~p"/members/#{@member}/edit?return_to=show"}>
<.icon name="hero-pencil-square" /> {gettext("Edit Member")}
@@ -37,6 +38,19 @@ defmodule MvWeb.MemberLive.Show do
<:item title={gettext("Street")}>{@member.street}
<:item title={gettext("House Number")}>{@member.house_number}
<:item title={gettext("Postal Code")}>{@member.postal_code}
+ <:item title={gettext("Linked User")}>
+ <%= if @member.user do %>
+ <.link
+ navigate={~p"/users/#{@member.user}"}
+ class="text-blue-600 hover:text-blue-800 underline"
+ >
+ <.icon name="hero-user" class="h-4 w-4 inline mr-1" />
+ {@member.user.email}
+
+ <% else %>
+ {gettext("No user linked")}
+ <% end %>
+
{gettext("Custom Properties")}
@@ -67,7 +81,7 @@ defmodule MvWeb.MemberLive.Show do
query =
Mv.Membership.Member
|> filter(id == ^id)
- |> load(properties: [:property_type])
+ |> load([:user, properties: [:property_type]])
member = Ash.read_one!(query)
diff --git a/lib/mv_web/live/user_live/show.ex b/lib/mv_web/live/user_live/show.ex
index 609a07c..bdd241b 100644
--- a/lib/mv_web/live/user_live/show.ex
+++ b/lib/mv_web/live/user_live/show.ex
@@ -10,8 +10,9 @@ defmodule MvWeb.UserLive.Show do
<:subtitle>{gettext("This is a user record from your database.")}
<:actions>
- <.button navigate={~p"/users"}>
+ <.button navigate={~p"/users"} aria-label={gettext("Back to users list")}>
<.icon name="hero-arrow-left" />
+ {gettext("Back to users list")}
<.button variant="primary" navigate={~p"/users/#{@user}/edit?return_to=show"}>
<.icon name="hero-pencil-square" /> {gettext("Edit User")}
@@ -26,6 +27,19 @@ defmodule MvWeb.UserLive.Show do
<:item title={gettext("Password Authentication")}>
{if @user.hashed_password, do: gettext("Enabled"), else: gettext("Not enabled")}
+ <:item title={gettext("Linked Member")}>
+ <%= if @user.member do %>
+ <.link
+ navigate={~p"/members/#{@user.member}"}
+ class="text-blue-600 hover:text-blue-800 underline"
+ >
+ <.icon name="hero-users" class="h-4 w-4 inline mr-1" />
+ {@user.member.first_name} {@user.member.last_name}
+
+ <% else %>
+ {gettext("No member linked")}
+ <% end %>
+
"""
@@ -33,9 +47,11 @@ defmodule MvWeb.UserLive.Show do
@impl true
def mount(%{"id" => id}, _session, socket) do
+ user = Ash.get!(Mv.Accounts.User, id, domain: Mv.Accounts, load: [:member])
+
{:ok,
socket
|> assign(:page_title, gettext("Show User"))
- |> assign(:user, Ash.get!(Mv.Accounts.User, id, domain: Mv.Accounts))}
+ |> assign(:user, user)}
end
end
diff --git a/priv/gettext/de/LC_MESSAGES/auth.po b/priv/gettext/de/LC_MESSAGES/auth.po
index 0f2202d..f7eef3e 100644
--- a/priv/gettext/de/LC_MESSAGES/auth.po
+++ b/priv/gettext/de/LC_MESSAGES/auth.po
@@ -27,10 +27,10 @@ msgid "Forgot your password?"
msgstr "Passwort vergessen?"
msgid "If this user exists in our database you will contacted with a sign-in link shortly."
-msgstr "Falls dieser Benutzer bekannt ist, wird jetzt eine Email mit Anmelde-Link versendet."
+msgstr "Falls diese*r Benutzer*in bekannt ist, wird jetzt eine Email mit Anmelde-Link versendet."
msgid "If this user exists in our system, you will be contacted with reset instructions shortly."
-msgstr "Falls dieser Benutzer bekannt ist, wird jetzt eine Email mit einer Anleitung zum Zurücksetzen versendet."
+msgstr "Falls diese*r Benutzer*in bekannt ist, wird jetzt eine Email mit einer Anleitung zum Zurücksetzen versendet."
msgid "Need an account?"
msgstr "Konto anlegen?"
@@ -61,6 +61,3 @@ msgstr "Anmelden..."
msgid "Your password has successfully been reset"
msgstr "Das Passwort wurde erfolgreich zurückgesetzt"
-
-msgid "Sign in with Rauthy"
-msgstr "Anmelden mit der Vereinscloud"
diff --git a/priv/gettext/de/LC_MESSAGES/default.po b/priv/gettext/de/LC_MESSAGES/default.po
index 1a7cf7e..bd86f61 100644
--- a/priv/gettext/de/LC_MESSAGES/default.po
+++ b/priv/gettext/de/LC_MESSAGES/default.po
@@ -15,69 +15,69 @@ msgstr ""
msgid "Actions"
msgstr "Aktionen"
-#: lib/mv_web/live/member_live/index.html.heex:77
+#: lib/mv_web/live/member_live/index.html.heex:84
#: lib/mv_web/live/user_live/index.html.heex:65
#, elixir-autogen, elixir-format
msgid "Are you sure?"
msgstr "Bist du sicher?"
-#: lib/mv_web/components/layouts.ex:71
-#: lib/mv_web/components/layouts.ex:83
+#: 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:25
-#: lib/mv_web/live/member_live/index.html.heex:62
-#: lib/mv_web/live/member_live/show.ex:36
+#: lib/mv_web/live/member_live/index.html.heex:69
+#: lib/mv_web/live/member_live/show.ex:37
#, elixir-autogen, elixir-format
msgid "City"
msgstr "Stadt"
-#: lib/mv_web/live/member_live/index.html.heex:79
+#: lib/mv_web/live/member_live/index.html.heex:86
#: lib/mv_web/live/user_live/index.html.heex:67
#, elixir-autogen, elixir-format
msgid "Delete"
msgstr "Löschen"
-#: lib/mv_web/live/member_live/index.html.heex:71
+#: lib/mv_web/live/member_live/index.html.heex:78
#: lib/mv_web/live/user_live/form.ex:109
#: lib/mv_web/live/user_live/index.html.heex:59
#, elixir-autogen, elixir-format
msgid "Edit"
msgstr "Bearbeite"
-#: lib/mv_web/live/member_live/show.ex:18
-#: lib/mv_web/live/member_live/show.ex:81
+#: lib/mv_web/live/member_live/show.ex:19
+#: lib/mv_web/live/member_live/show.ex:95
#, elixir-autogen, elixir-format
msgid "Edit Member"
msgstr "Mitglied bearbeiten"
#: lib/mv_web/live/member_live/form.ex:18
-#: lib/mv_web/live/member_live/index.html.heex:58
-#: lib/mv_web/live/member_live/show.ex:27
+#: lib/mv_web/live/member_live/index.html.heex:65
+#: lib/mv_web/live/member_live/show.ex:28
#: lib/mv_web/live/user_live/form.ex:14
#: lib/mv_web/live/user_live/index.html.heex:44
-#: lib/mv_web/live/user_live/show.ex:24
+#: lib/mv_web/live/user_live/show.ex:25
#, elixir-autogen, elixir-format
msgid "Email"
msgstr "E-Mail"
#: lib/mv_web/live/member_live/form.ex:16
-#: lib/mv_web/live/member_live/show.ex:25
+#: lib/mv_web/live/member_live/show.ex:26
#, elixir-autogen, elixir-format
msgid "First Name"
msgstr "Vorname"
#: lib/mv_web/live/member_live/form.ex:22
-#: lib/mv_web/live/member_live/index.html.heex:64
-#: lib/mv_web/live/member_live/show.ex:33
+#: lib/mv_web/live/member_live/index.html.heex:71
+#: lib/mv_web/live/member_live/show.ex:34
#, elixir-autogen, elixir-format
msgid "Join Date"
msgstr "Beitrittsdatum"
#: lib/mv_web/live/member_live/form.ex:17
-#: lib/mv_web/live/member_live/show.ex:26
+#: lib/mv_web/live/member_live/show.ex:27
#, elixir-autogen, elixir-format
msgid "Last Name"
msgstr "Nachname"
@@ -87,18 +87,18 @@ msgstr "Nachname"
msgid "New Member"
msgstr "Neues Mitglied"
-#: lib/mv_web/live/member_live/index.html.heex:68
+#: lib/mv_web/live/member_live/index.html.heex:75
#: lib/mv_web/live/user_live/index.html.heex:56
#, elixir-autogen, elixir-format
msgid "Show"
msgstr "Anzeigen"
-#: lib/mv_web/components/layouts.ex:78
+#: 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:66
+#: lib/mv_web/components/layouts.ex:75
#, elixir-autogen, elixir-format
msgid "We can't find the internet"
msgstr "Keine Internetverbindung gefunden"
@@ -109,52 +109,52 @@ msgid "close"
msgstr "schließen"
#: lib/mv_web/live/member_live/form.ex:19
-#: lib/mv_web/live/member_live/show.ex:28
+#: lib/mv_web/live/member_live/show.ex:29
#, elixir-autogen, elixir-format
msgid "Birth Date"
msgstr "Geburtsdatum"
#: lib/mv_web/live/member_live/form.ex:30
-#: lib/mv_web/live/member_live/show.ex:42
+#: lib/mv_web/live/member_live/show.ex:56
#, elixir-autogen, elixir-format
msgid "Custom Properties"
msgstr "Eigene Eigenschaften"
#: lib/mv_web/live/member_live/form.ex:23
-#: lib/mv_web/live/member_live/show.ex:34
+#: lib/mv_web/live/member_live/show.ex:35
#, elixir-autogen, elixir-format
msgid "Exit Date"
msgstr "Austrittsdatum"
#: lib/mv_web/live/member_live/form.ex:27
-#: lib/mv_web/live/member_live/index.html.heex:60
-#: lib/mv_web/live/member_live/show.ex:38
+#: lib/mv_web/live/member_live/index.html.heex:67
+#: lib/mv_web/live/member_live/show.ex:39
#, elixir-autogen, elixir-format
msgid "House Number"
msgstr "Hausnummer"
#: lib/mv_web/live/member_live/form.ex:24
-#: lib/mv_web/live/member_live/show.ex:35
+#: lib/mv_web/live/member_live/show.ex:36
#, elixir-autogen, elixir-format
msgid "Notes"
msgstr "Notizen"
#: lib/mv_web/live/member_live/form.ex:20
-#: lib/mv_web/live/member_live/show.ex:29
+#: lib/mv_web/live/member_live/show.ex:30
#, elixir-autogen, elixir-format
msgid "Paid"
msgstr "Bezahlt"
#: lib/mv_web/live/member_live/form.ex:21
-#: lib/mv_web/live/member_live/index.html.heex:63
-#: lib/mv_web/live/member_live/show.ex:32
+#: lib/mv_web/live/member_live/index.html.heex:70
+#: lib/mv_web/live/member_live/show.ex:33
#, elixir-autogen, elixir-format
msgid "Phone Number"
msgstr "Telefonnummer"
#: lib/mv_web/live/member_live/form.ex:28
-#: lib/mv_web/live/member_live/index.html.heex:61
-#: lib/mv_web/live/member_live/show.ex:39
+#: lib/mv_web/live/member_live/index.html.heex:68
+#: lib/mv_web/live/member_live/show.ex:40
#, elixir-autogen, elixir-format
msgid "Postal Code"
msgstr "Postleitzahl"
@@ -173,8 +173,8 @@ msgid "Saving..."
msgstr "Speichern..."
#: lib/mv_web/live/member_live/form.ex:26
-#: lib/mv_web/live/member_live/index.html.heex:59
-#: lib/mv_web/live/member_live/show.ex:37
+#: lib/mv_web/live/member_live/index.html.heex:66
+#: lib/mv_web/live/member_live/show.ex:38
#, elixir-autogen, elixir-format
msgid "Street"
msgstr "Straße"
@@ -184,17 +184,17 @@ msgstr "Straße"
msgid "Use this form to manage member records and their properties."
msgstr "Dieses Formular dient zur Verwaltung von Mitgliedern und deren Eigenschaften."
-#: lib/mv_web/live/member_live/show.ex:24
+#: lib/mv_web/live/member_live/show.ex:25
#, elixir-autogen, elixir-format
msgid "Id"
msgstr "ID"
-#: lib/mv_web/live/member_live/show.ex:30
+#: lib/mv_web/live/member_live/show.ex:31
#, elixir-autogen, elixir-format
msgid "No"
msgstr "Nein"
-#: lib/mv_web/live/member_live/show.ex:80
+#: lib/mv_web/live/member_live/show.ex:94
#, elixir-autogen, elixir-format, fuzzy
msgid "Show Member"
msgstr "Mitglied anzeigen"
@@ -204,7 +204,7 @@ 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/show.ex:30
+#: lib/mv_web/live/member_live/show.ex:31
#, elixir-autogen, elixir-format
msgid "Yes"
msgstr "Ja"
@@ -281,17 +281,17 @@ msgstr "Eigenschaftstyp auswählen"
msgid "Description"
msgstr "Beschreibung"
-#: lib/mv_web/live/user_live/show.ex:17
+#: lib/mv_web/live/user_live/show.ex:18
#, elixir-autogen, elixir-format
msgid "Edit User"
-msgstr "Benutzer bearbeiten"
+msgstr "Benutzer*in bearbeiten"
-#: lib/mv_web/live/user_live/show.ex:27
+#: lib/mv_web/live/user_live/show.ex:28
#, elixir-autogen, elixir-format
msgid "Enabled"
msgstr "Aktiviert"
-#: lib/mv_web/live/user_live/show.ex:23
+#: lib/mv_web/live/user_live/show.ex:24
#, elixir-autogen, elixir-format
msgid "ID"
msgstr "ID"
@@ -301,7 +301,7 @@ msgstr "ID"
msgid "Immutable"
msgstr "Unveränderlich"
-#: lib/mv_web/components/layouts/navbar.ex:73
+#: lib/mv_web/components/layouts/navbar.ex:94
#, elixir-autogen, elixir-format
msgid "Logout"
msgstr "Abmelden"
@@ -310,21 +310,21 @@ msgstr "Abmelden"
#: lib/mv_web/live/user_live/index.html.heex:3
#, elixir-autogen, elixir-format
msgid "Listing Users"
-msgstr "Benutzer auflisten"
+msgstr "Benutzer*innen auflisten"
#: lib/mv_web/live/property_live/form.ex:27
#, elixir-autogen, elixir-format
msgid "Member"
msgstr "Mitglied"
-#: lib/mv_web/components/layouts/navbar.ex:14
-#: lib/mv_web/live/member_live/index.ex:12
+#: lib/mv_web/components/layouts/navbar.ex:19
+#: lib/mv_web/live/member_live/index.ex:14
#: lib/mv_web/live/member_live/index.html.heex:3
#, elixir-autogen, elixir-format
msgid "Members"
msgstr "Mitglieder"
-#: lib/mv_web/live/member_live/index.html.heex:50
+#: lib/mv_web/live/member_live/index.html.heex:57
#: lib/mv_web/live/property_type_live/form.ex:16
#, elixir-autogen, elixir-format
msgid "Name"
@@ -333,14 +333,14 @@ msgstr "Name"
#: lib/mv_web/live/user_live/index.html.heex:6
#, elixir-autogen, elixir-format
msgid "New User"
-msgstr "Neuer Benutzer"
+msgstr "Neue*r Benutzer*in"
-#: lib/mv_web/live/user_live/show.ex:27
+#: lib/mv_web/live/user_live/show.ex:28
#, elixir-autogen, elixir-format
msgid "Not enabled"
msgstr "Nicht aktiviert"
-#: lib/mv_web/live/user_live/show.ex:25
+#: lib/mv_web/live/user_live/show.ex:26
#, elixir-autogen, elixir-format
msgid "Not set"
msgstr "Nicht gesetzt"
@@ -352,12 +352,12 @@ msgid "Note"
msgstr "Hinweis"
#: lib/mv_web/live/user_live/index.html.heex:52
-#: lib/mv_web/live/user_live/show.ex:25
+#: lib/mv_web/live/user_live/show.ex:26
#, elixir-autogen, elixir-format
msgid "OIDC ID"
msgstr "OIDC ID"
-#: lib/mv_web/live/user_live/show.ex:26
+#: lib/mv_web/live/user_live/show.ex:27
#, elixir-autogen, elixir-format
msgid "Password Authentication"
msgstr "Passwort-Authentifizierung"
@@ -367,15 +367,15 @@ msgstr "Passwort-Authentifizierung"
msgid "Please select a property type first"
msgstr "Bitte wählen Sie zuerst einen Eigenschaftstyp"
-#: lib/mv_web/components/layouts/navbar.ex:69
+#: lib/mv_web/components/layouts/navbar.ex:89
#, elixir-autogen, elixir-format
msgid "Profil"
msgstr "Profil"
#: lib/mv_web/live/property_live/form.ex:207
-#, elixir-autogen, elixir-format, fuzzy
+#, elixir-autogen, elixir-format
msgid "Property %{action} successfully"
-msgstr "Mitglied %{action} erfolgreich"
+msgstr "Eigenschaft %{action} erfolgreich"
#: lib/mv_web/live/property_live/form.ex:18
#, elixir-autogen, elixir-format
@@ -402,17 +402,17 @@ msgstr "Eigenschaft speichern"
msgid "Save Property type"
msgstr "Eigenschaftstyp speichern"
-#: lib/mv_web/live/member_live/index.html.heex:27
+#: lib/mv_web/live/member_live/index.html.heex:34
#, elixir-autogen, elixir-format
msgid "Select all members"
msgstr "Alle Mitglieder auswählen"
-#: lib/mv_web/live/member_live/index.html.heex:41
+#: lib/mv_web/live/member_live/index.html.heex:48
#, elixir-autogen, elixir-format
msgid "Select member"
msgstr "Mitglied auswählen"
-#: lib/mv_web/components/layouts/navbar.ex:72
+#: lib/mv_web/components/layouts/navbar.ex:92
#, elixir-autogen, elixir-format
msgid "Settings"
msgstr "Einstellungen"
@@ -420,17 +420,17 @@ msgstr "Einstellungen"
#: lib/mv_web/live/user_live/form.ex:93
#, elixir-autogen, elixir-format
msgid "Save User"
-msgstr "Benutzer speichern"
+msgstr "Benutzer*in speichern"
-#: lib/mv_web/live/user_live/show.ex:38
+#: lib/mv_web/live/user_live/show.ex:54
#, elixir-autogen, elixir-format
msgid "Show User"
-msgstr "Benutzer anzeigen"
+msgstr "Benutzer*in anzeigen"
#: lib/mv_web/live/user_live/show.ex:10
#, elixir-autogen, elixir-format
msgid "This is a user record from your database."
-msgstr "Dies ist ein Benutzer-Datensatz aus Ihrer Datenbank."
+msgstr "Dies ist ein Benutzer*innen-Datensatz aus Ihrer Datenbank."
#: lib/mv_web/live/property_live/form.ex:95
#, elixir-autogen, elixir-format
@@ -438,25 +438,25 @@ msgid "Unsupported value type: %{type}"
msgstr "Nicht unterstützter Wertetyp: %{type}"
#: lib/mv_web/live/property_live/form.ex:10
-#, elixir-autogen, elixir-format, fuzzy
+#, elixir-autogen, elixir-format
msgid "Use this form to manage property records in your database."
-msgstr "Dieses Formular dient zur Verwaltung von Mitgliedern und deren Eigenschaften."
+msgstr "Dieses Formular dient zur Verwaltung von Eigenschaften in der Datenbank."
#: lib/mv_web/live/property_type_live/form.ex:11
-#, elixir-autogen, elixir-format, fuzzy
+#, elixir-autogen, elixir-format
msgid "Use this form to manage property_type records in your database."
-msgstr "Dieses Formular dient zur Verwaltung von Mitgliedern und deren Eigenschaften."
+msgstr "Dieses Formular dient zur Verwaltung von Eigenschaftstypen in der Datenbank."
#: lib/mv_web/live/user_live/form.ex:10
#, elixir-autogen, elixir-format
msgid "Use this form to manage user records in your database."
-msgstr "Verwenden Sie dieses Formular, um Benutzer-Datensätze zu verwalten."
+msgstr "Verwenden Sie dieses Formular, um Benutzer*innen-Datensätze zu verwalten."
#: lib/mv_web/live/user_live/form.ex:110
#: lib/mv_web/live/user_live/show.ex:9
#, elixir-autogen, elixir-format
msgid "User"
-msgstr "Benutzer"
+msgstr "Benutzer*in"
#: lib/mv_web/live/property_live/form.ex:59
#, elixir-autogen, elixir-format
@@ -481,17 +481,17 @@ msgstr "absteigend"
#: lib/mv_web/live/user_live/form.ex:109
#, elixir-autogen, elixir-format
msgid "New"
-msgstr "Neuer"
+msgstr "Neue*r"
#: lib/mv_web/live/user_live/form.ex:64
#, elixir-autogen, elixir-format
msgid "Admin Note"
-msgstr "Administrator-Hinweis"
+msgstr "Administrator*innen-Hinweis"
#: lib/mv_web/live/user_live/form.ex:64
#, elixir-autogen, elixir-format
msgid "As an administrator, you can directly set a new password for this user using the same secure Ash Authentication system."
-msgstr "Als Administrator können Sie direkt ein neues Passwort für diesen Benutzer setzen, wobei das gleiche sichere Ash Authentication System verwendet wird."
+msgstr "Als Administrator*in können Sie direkt ein neues Passwort für diese*n Benutzer*in setzen, wobei das gleiche sichere Ash Authentication System verwendet wird."
#: lib/mv_web/live/user_live/form.ex:55
#, elixir-autogen, elixir-format
@@ -506,7 +506,7 @@ msgstr "Passwort ändern"
#: lib/mv_web/live/user_live/form.ex:75
#, elixir-autogen, elixir-format
msgid "Check 'Change Password' above to set a new password for this user."
-msgstr "Aktivieren Sie 'Passwort ändern' oben, um ein neues Passwort für diesen Benutzer zu setzen."
+msgstr "Aktivieren Sie 'Passwort ändern' oben, um ein neues Passwort für diese*n Benutzer*in zu setzen."
#: lib/mv_web/live/user_live/form.ex:45
#, elixir-autogen, elixir-format
@@ -536,12 +536,12 @@ msgstr "Passwort-Anforderungen"
#: lib/mv_web/live/user_live/index.html.heex:21
#, elixir-autogen, elixir-format
msgid "Select all users"
-msgstr "Alle Benutzer auswählen"
+msgstr "Alle Benutzer*innen auswählen"
#: lib/mv_web/live/user_live/index.html.heex:35
#, elixir-autogen, elixir-format
msgid "Select user"
-msgstr "Benutzer auswählen"
+msgstr "Benutzer*in auswählen"
#: lib/mv_web/live/user_live/form.ex:27
#, elixir-autogen, elixir-format
@@ -551,9 +551,59 @@ msgstr "Passwort setzen"
#: lib/mv_web/live/user_live/form.ex:83
#, elixir-autogen, elixir-format
msgid "User will be created without a password. Check 'Set Password' to add one."
-msgstr "Benutzer wird ohne Passwort erstellt. Aktivieren Sie 'Passwort setzen', um eines hinzuzufügen."
+msgstr "Benutzer*in wird ohne Passwort erstellt. Aktivieren Sie 'Passwort setzen', um eines hinzuzufügen."
-#: lib/mv_web/auth_overrides.ex:30
+#: lib/mv_web/live/user_live/show.ex:30
#, elixir-autogen, elixir-format
-msgid "or"
-msgstr "oder"
+msgid "Linked Member"
+msgstr "Verknüpftes Mitglied"
+
+#: lib/mv_web/live/member_live/show.ex:41
+#, elixir-autogen, elixir-format
+msgid "Linked User"
+msgstr "Verknüpfte*r Benutzer*in"
+
+#: lib/mv_web/live/user_live/show.ex:40
+#, elixir-autogen, elixir-format
+msgid "No member linked"
+msgstr "Kein Mitglied verknüpft"
+
+#: lib/mv_web/live/member_live/show.ex:51
+#, elixir-autogen, elixir-format
+msgid "No user linked"
+msgstr "Keine*r Benutzer*in verknüpft"
+
+#: lib/mv_web/live/member_live/show.ex:14
+#: lib/mv_web/live/member_live/show.ex:16
+#, elixir-autogen, elixir-format
+msgid "Back to members list"
+msgstr "Zurück zur Mitgliederliste"
+
+#: lib/mv_web/live/user_live/show.ex:13
+#: lib/mv_web/live/user_live/show.ex:15
+#, elixir-autogen, elixir-format
+msgid "Back to users list"
+msgstr "Zurück zur Benutzer*innen-Liste"
+
+#: lib/mv_web/components/layouts/navbar.ex:27
+#: lib/mv_web/components/layouts/navbar.ex:33
+#, elixir-autogen, elixir-format
+msgid "Select language"
+msgstr "Sprache auswählen"
+
+#: lib/mv_web/components/layouts/navbar.ex:40
+#: lib/mv_web/components/layouts/navbar.ex:60
+#, elixir-autogen, elixir-format
+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:15
+#, elixir-autogen, elixir-format
+msgid "Search..."
+msgstr ""
+
+#: lib/mv_web/components/layouts/navbar.ex:20
+#, elixir-autogen, elixir-format, fuzzy
+msgid "Users"
+msgstr "Benutzer*innen"
diff --git a/priv/gettext/de/LC_MESSAGES/errors.po b/priv/gettext/de/LC_MESSAGES/errors.po
index 844c4f5..e0db8dd 100644
--- a/priv/gettext/de/LC_MESSAGES/errors.po
+++ b/priv/gettext/de/LC_MESSAGES/errors.po
@@ -12,101 +12,146 @@ msgstr ""
## From Ecto.Changeset.cast/4
msgid "can't be blank"
-msgstr ""
+msgstr "darf nicht leer sein"
## From Ecto.Changeset.unique_constraint/3
msgid "has already been taken"
-msgstr ""
+msgstr "ist bereits vergeben"
## From Ecto.Changeset.put_change/3
msgid "is invalid"
-msgstr ""
+msgstr "ist ungültig"
## From Ecto.Changeset.validate_acceptance/3
msgid "must be accepted"
-msgstr ""
+msgstr "muss akzeptiert werden"
## From Ecto.Changeset.validate_format/3
msgid "has invalid format"
-msgstr ""
+msgstr "hat ein ungültiges Format"
## From Ecto.Changeset.validate_subset/3
msgid "has an invalid entry"
-msgstr ""
+msgstr "hat einen ungültigen Eintrag"
## From Ecto.Changeset.validate_exclusion/3
msgid "is reserved"
-msgstr ""
+msgstr "ist reserviert"
## From Ecto.Changeset.validate_confirmation/3
msgid "does not match confirmation"
-msgstr ""
+msgstr "stimmt nicht mit der Bestätigung überein"
## From Ecto.Changeset.no_assoc_constraint/3
msgid "is still associated with this entry"
-msgstr ""
+msgstr "ist noch mit diesem Eintrag verknüpft"
msgid "are still associated with this entry"
-msgstr ""
+msgstr "sind noch mit diesem Eintrag verknüpft"
## From Ecto.Changeset.validate_length/3
msgid "should have %{count} item(s)"
msgid_plural "should have %{count} item(s)"
-msgstr[0] ""
-msgstr[1] ""
+msgstr[0] "sollte %{count} Element haben"
+msgstr[1] "sollte %{count} Elemente haben"
msgid "should be %{count} character(s)"
msgid_plural "should be %{count} character(s)"
-msgstr[0] ""
-msgstr[1] ""
+msgstr[0] "sollte %{count} Zeichen haben"
+msgstr[1] "sollte %{count} Zeichen haben"
msgid "should be %{count} byte(s)"
msgid_plural "should be %{count} byte(s)"
-msgstr[0] ""
-msgstr[1] ""
+msgstr[0] "sollte %{count} Byte haben"
+msgstr[1] "sollte %{count} Bytes haben"
msgid "should have at least %{count} item(s)"
msgid_plural "should have at least %{count} item(s)"
-msgstr[0] ""
-msgstr[1] ""
+msgstr[0] "sollte mindestens %{count} Element haben"
+msgstr[1] "sollte mindestens %{count} Elemente haben"
msgid "should be at least %{count} character(s)"
msgid_plural "should be at least %{count} character(s)"
-msgstr[0] ""
-msgstr[1] ""
+msgstr[0] "sollte mindestens %{count} Zeichen haben"
+msgstr[1] "sollte mindestens %{count} Zeichen haben"
msgid "should be at least %{count} byte(s)"
msgid_plural "should be at least %{count} byte(s)"
-msgstr[0] ""
-msgstr[1] ""
+msgstr[0] "sollte mindestens %{count} Byte haben"
+msgstr[1] "sollte mindestens %{count} Bytes haben"
msgid "should have at most %{count} item(s)"
msgid_plural "should have at most %{count} item(s)"
-msgstr[0] ""
-msgstr[1] ""
+msgstr[0] "sollte höchstens %{count} Element haben"
+msgstr[1] "sollte höchstens %{count} Elemente haben"
msgid "should be at most %{count} character(s)"
msgid_plural "should be at most %{count} character(s)"
-msgstr[0] ""
-msgstr[1] ""
+msgstr[0] "sollte höchstens %{count} Zeichen haben"
+msgstr[1] "sollte höchstens %{count} Zeichen haben"
msgid "should be at most %{count} byte(s)"
msgid_plural "should be at most %{count} byte(s)"
-msgstr[0] ""
-msgstr[1] ""
+msgstr[0] "sollte höchstens %{count} Byte haben"
+msgstr[1] "sollte höchstens %{count} Bytes haben"
## From Ecto.Changeset.validate_number/3
msgid "must be less than %{number}"
-msgstr ""
+msgstr "muss kleiner als %{number} sein"
msgid "must be greater than %{number}"
-msgstr ""
+msgstr "muss größer als %{number} sein"
msgid "must be less than or equal to %{number}"
-msgstr ""
+msgstr "muss kleiner oder gleich %{number} sein"
msgid "must be greater than or equal to %{number}"
-msgstr ""
+msgstr "muss größer oder gleich %{number} sein"
msgid "must be equal to %{number}"
-msgstr ""
+msgstr "muss gleich %{number} sein"
+
+## Ash Framework - Standard constraint messages
+msgid "length must be greater than or equal to %{min}"
+msgstr "muss mindestens %{min} Zeichen lang sein"
+
+msgid "length must be less than or equal to %{max}"
+msgstr "darf höchstens %{max} Zeichen lang sein"
+
+msgid "must be present"
+msgstr "muss vorhanden sein"
+
+## Custom validation messages from Mv.Accounts.User
+msgid "User already has a member. Remove existing member first."
+msgstr "Benutzer*in hat bereits ein Mitglied. Entfernen Sie zuerst das vorhandene Mitglied."
+
+msgid "OIDC user_info must contain a non-empty 'sub' or 'id' field"
+msgstr "OIDC user_info darf kein leeres 'sub' oder 'id' Feld enthalten"
+
+## Custom validation messages from Mv.Membership.Member
+msgid "User is already linked to another member"
+msgstr "Benutzer*in ist bereits mit einem anderen Mitglied verknüpft"
+
+msgid "User not found"
+msgstr "Benutzer*in nicht gefunden"
+
+msgid "cannot be in the future"
+msgstr "darf nicht in der Zukunft liegen"
+
+msgid "cannot be before join date"
+msgstr "darf nicht vor dem Beitrittsdatum liegen"
+
+msgid "is not a valid phone number"
+msgstr "ist keine gültige Telefonnummer"
+
+msgid "must consist of 5 digits"
+msgstr "muss aus 5 Ziffern bestehen"
+
+msgid "is not a valid email"
+msgstr "ist keine gültige E-Mail-Adresse"
+
+msgid "must have length of at least 8"
+msgstr "muss mindestens 8 Zeichen lang sein"
+
+msgid "is required"
+msgstr "ist erforderlich"
diff --git a/priv/gettext/default.pot b/priv/gettext/default.pot
index a9bfb08..93c5d95 100644
--- a/priv/gettext/default.pot
+++ b/priv/gettext/default.pot
@@ -16,69 +16,69 @@ msgstr ""
msgid "Actions"
msgstr ""
-#: lib/mv_web/live/member_live/index.html.heex:77
+#: lib/mv_web/live/member_live/index.html.heex:84
#: lib/mv_web/live/user_live/index.html.heex:65
#, elixir-autogen, elixir-format
msgid "Are you sure?"
msgstr ""
-#: lib/mv_web/components/layouts.ex:71
-#: lib/mv_web/components/layouts.ex:83
+#: 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:25
-#: lib/mv_web/live/member_live/index.html.heex:62
-#: lib/mv_web/live/member_live/show.ex:36
+#: lib/mv_web/live/member_live/index.html.heex:69
+#: lib/mv_web/live/member_live/show.ex:37
#, elixir-autogen, elixir-format
msgid "City"
msgstr ""
-#: lib/mv_web/live/member_live/index.html.heex:79
+#: lib/mv_web/live/member_live/index.html.heex:86
#: lib/mv_web/live/user_live/index.html.heex:67
#, elixir-autogen, elixir-format
msgid "Delete"
msgstr ""
-#: lib/mv_web/live/member_live/index.html.heex:71
+#: lib/mv_web/live/member_live/index.html.heex:78
#: lib/mv_web/live/user_live/form.ex:109
#: lib/mv_web/live/user_live/index.html.heex:59
#, elixir-autogen, elixir-format
msgid "Edit"
msgstr ""
-#: lib/mv_web/live/member_live/show.ex:18
-#: lib/mv_web/live/member_live/show.ex:81
+#: lib/mv_web/live/member_live/show.ex:19
+#: lib/mv_web/live/member_live/show.ex:95
#, elixir-autogen, elixir-format
msgid "Edit Member"
msgstr ""
#: lib/mv_web/live/member_live/form.ex:18
-#: lib/mv_web/live/member_live/index.html.heex:58
-#: lib/mv_web/live/member_live/show.ex:27
+#: lib/mv_web/live/member_live/index.html.heex:65
+#: lib/mv_web/live/member_live/show.ex:28
#: lib/mv_web/live/user_live/form.ex:14
#: lib/mv_web/live/user_live/index.html.heex:44
-#: lib/mv_web/live/user_live/show.ex:24
+#: lib/mv_web/live/user_live/show.ex:25
#, elixir-autogen, elixir-format
msgid "Email"
msgstr ""
#: lib/mv_web/live/member_live/form.ex:16
-#: lib/mv_web/live/member_live/show.ex:25
+#: lib/mv_web/live/member_live/show.ex:26
#, elixir-autogen, elixir-format
msgid "First Name"
msgstr ""
#: lib/mv_web/live/member_live/form.ex:22
-#: lib/mv_web/live/member_live/index.html.heex:64
-#: lib/mv_web/live/member_live/show.ex:33
+#: lib/mv_web/live/member_live/index.html.heex:71
+#: lib/mv_web/live/member_live/show.ex:34
#, elixir-autogen, elixir-format
msgid "Join Date"
msgstr ""
#: lib/mv_web/live/member_live/form.ex:17
-#: lib/mv_web/live/member_live/show.ex:26
+#: lib/mv_web/live/member_live/show.ex:27
#, elixir-autogen, elixir-format
msgid "Last Name"
msgstr ""
@@ -88,18 +88,18 @@ msgstr ""
msgid "New Member"
msgstr ""
-#: lib/mv_web/live/member_live/index.html.heex:68
+#: lib/mv_web/live/member_live/index.html.heex:75
#: lib/mv_web/live/user_live/index.html.heex:56
#, elixir-autogen, elixir-format
msgid "Show"
msgstr ""
-#: lib/mv_web/components/layouts.ex:78
+#: lib/mv_web/components/layouts.ex:87
#, elixir-autogen, elixir-format
msgid "Something went wrong!"
msgstr ""
-#: lib/mv_web/components/layouts.ex:66
+#: lib/mv_web/components/layouts.ex:75
#, elixir-autogen, elixir-format
msgid "We can't find the internet"
msgstr ""
@@ -110,52 +110,52 @@ msgid "close"
msgstr ""
#: lib/mv_web/live/member_live/form.ex:19
-#: lib/mv_web/live/member_live/show.ex:28
+#: lib/mv_web/live/member_live/show.ex:29
#, elixir-autogen, elixir-format
msgid "Birth Date"
msgstr ""
#: lib/mv_web/live/member_live/form.ex:30
-#: lib/mv_web/live/member_live/show.ex:42
+#: lib/mv_web/live/member_live/show.ex:56
#, elixir-autogen, elixir-format
msgid "Custom Properties"
msgstr ""
#: lib/mv_web/live/member_live/form.ex:23
-#: lib/mv_web/live/member_live/show.ex:34
+#: lib/mv_web/live/member_live/show.ex:35
#, elixir-autogen, elixir-format
msgid "Exit Date"
msgstr ""
#: lib/mv_web/live/member_live/form.ex:27
-#: lib/mv_web/live/member_live/index.html.heex:60
-#: lib/mv_web/live/member_live/show.ex:38
+#: lib/mv_web/live/member_live/index.html.heex:67
+#: lib/mv_web/live/member_live/show.ex:39
#, elixir-autogen, elixir-format
msgid "House Number"
msgstr ""
#: lib/mv_web/live/member_live/form.ex:24
-#: lib/mv_web/live/member_live/show.ex:35
+#: lib/mv_web/live/member_live/show.ex:36
#, elixir-autogen, elixir-format
msgid "Notes"
msgstr ""
#: lib/mv_web/live/member_live/form.ex:20
-#: lib/mv_web/live/member_live/show.ex:29
+#: lib/mv_web/live/member_live/show.ex:30
#, elixir-autogen, elixir-format
msgid "Paid"
msgstr ""
#: lib/mv_web/live/member_live/form.ex:21
-#: lib/mv_web/live/member_live/index.html.heex:63
-#: lib/mv_web/live/member_live/show.ex:32
+#: lib/mv_web/live/member_live/index.html.heex:70
+#: lib/mv_web/live/member_live/show.ex:33
#, elixir-autogen, elixir-format
msgid "Phone Number"
msgstr ""
#: lib/mv_web/live/member_live/form.ex:28
-#: lib/mv_web/live/member_live/index.html.heex:61
-#: lib/mv_web/live/member_live/show.ex:39
+#: lib/mv_web/live/member_live/index.html.heex:68
+#: lib/mv_web/live/member_live/show.ex:40
#, elixir-autogen, elixir-format
msgid "Postal Code"
msgstr ""
@@ -174,8 +174,8 @@ msgid "Saving..."
msgstr ""
#: lib/mv_web/live/member_live/form.ex:26
-#: lib/mv_web/live/member_live/index.html.heex:59
-#: lib/mv_web/live/member_live/show.ex:37
+#: lib/mv_web/live/member_live/index.html.heex:66
+#: lib/mv_web/live/member_live/show.ex:38
#, elixir-autogen, elixir-format
msgid "Street"
msgstr ""
@@ -185,17 +185,17 @@ msgstr ""
msgid "Use this form to manage member records and their properties."
msgstr ""
-#: lib/mv_web/live/member_live/show.ex:24
+#: lib/mv_web/live/member_live/show.ex:25
#, elixir-autogen, elixir-format
msgid "Id"
msgstr ""
-#: lib/mv_web/live/member_live/show.ex:30
+#: lib/mv_web/live/member_live/show.ex:31
#, elixir-autogen, elixir-format
msgid "No"
msgstr ""
-#: lib/mv_web/live/member_live/show.ex:80
+#: lib/mv_web/live/member_live/show.ex:94
#, elixir-autogen, elixir-format
msgid "Show Member"
msgstr ""
@@ -205,7 +205,7 @@ msgstr ""
msgid "This is a member record from your database."
msgstr ""
-#: lib/mv_web/live/member_live/show.ex:30
+#: lib/mv_web/live/member_live/show.ex:31
#, elixir-autogen, elixir-format
msgid "Yes"
msgstr ""
@@ -282,17 +282,17 @@ msgstr ""
msgid "Description"
msgstr ""
-#: lib/mv_web/live/user_live/show.ex:17
+#: lib/mv_web/live/user_live/show.ex:18
#, elixir-autogen, elixir-format
msgid "Edit User"
msgstr ""
-#: lib/mv_web/live/user_live/show.ex:27
+#: lib/mv_web/live/user_live/show.ex:28
#, elixir-autogen, elixir-format
msgid "Enabled"
msgstr ""
-#: lib/mv_web/live/user_live/show.ex:23
+#: lib/mv_web/live/user_live/show.ex:24
#, elixir-autogen, elixir-format
msgid "ID"
msgstr ""
@@ -302,7 +302,7 @@ msgstr ""
msgid "Immutable"
msgstr ""
-#: lib/mv_web/components/layouts/navbar.ex:73
+#: lib/mv_web/components/layouts/navbar.ex:94
#, elixir-autogen, elixir-format
msgid "Logout"
msgstr ""
@@ -318,14 +318,14 @@ msgstr ""
msgid "Member"
msgstr ""
-#: lib/mv_web/components/layouts/navbar.ex:14
-#: lib/mv_web/live/member_live/index.ex:12
+#: lib/mv_web/components/layouts/navbar.ex:19
+#: lib/mv_web/live/member_live/index.ex:14
#: lib/mv_web/live/member_live/index.html.heex:3
#, elixir-autogen, elixir-format
msgid "Members"
msgstr ""
-#: lib/mv_web/live/member_live/index.html.heex:50
+#: lib/mv_web/live/member_live/index.html.heex:57
#: lib/mv_web/live/property_type_live/form.ex:16
#, elixir-autogen, elixir-format
msgid "Name"
@@ -336,12 +336,12 @@ msgstr ""
msgid "New User"
msgstr ""
-#: lib/mv_web/live/user_live/show.ex:27
+#: lib/mv_web/live/user_live/show.ex:28
#, elixir-autogen, elixir-format
msgid "Not enabled"
msgstr ""
-#: lib/mv_web/live/user_live/show.ex:25
+#: lib/mv_web/live/user_live/show.ex:26
#, elixir-autogen, elixir-format
msgid "Not set"
msgstr ""
@@ -353,12 +353,12 @@ msgid "Note"
msgstr ""
#: lib/mv_web/live/user_live/index.html.heex:52
-#: lib/mv_web/live/user_live/show.ex:25
+#: lib/mv_web/live/user_live/show.ex:26
#, elixir-autogen, elixir-format
msgid "OIDC ID"
msgstr ""
-#: lib/mv_web/live/user_live/show.ex:26
+#: lib/mv_web/live/user_live/show.ex:27
#, elixir-autogen, elixir-format
msgid "Password Authentication"
msgstr ""
@@ -368,7 +368,7 @@ msgstr ""
msgid "Please select a property type first"
msgstr ""
-#: lib/mv_web/components/layouts/navbar.ex:69
+#: lib/mv_web/components/layouts/navbar.ex:89
#, elixir-autogen, elixir-format
msgid "Profil"
msgstr ""
@@ -403,17 +403,17 @@ msgstr ""
msgid "Save Property type"
msgstr ""
-#: lib/mv_web/live/member_live/index.html.heex:27
+#: lib/mv_web/live/member_live/index.html.heex:34
#, elixir-autogen, elixir-format
msgid "Select all members"
msgstr ""
-#: lib/mv_web/live/member_live/index.html.heex:41
+#: lib/mv_web/live/member_live/index.html.heex:48
#, elixir-autogen, elixir-format
msgid "Select member"
msgstr ""
-#: lib/mv_web/components/layouts/navbar.ex:72
+#: lib/mv_web/components/layouts/navbar.ex:92
#, elixir-autogen, elixir-format
msgid "Settings"
msgstr ""
@@ -423,7 +423,7 @@ msgstr ""
msgid "Save User"
msgstr ""
-#: lib/mv_web/live/user_live/show.ex:38
+#: lib/mv_web/live/user_live/show.ex:54
#, elixir-autogen, elixir-format
msgid "Show User"
msgstr ""
@@ -554,7 +554,57 @@ msgstr ""
msgid "User will be created without a password. Check 'Set Password' to add one."
msgstr ""
-#: lib/mv_web/auth_overrides.ex:30
+#: lib/mv_web/live/user_live/show.ex:30
#, elixir-autogen, elixir-format
-msgid "or"
+msgid "Linked Member"
+msgstr ""
+
+#: lib/mv_web/live/member_live/show.ex:41
+#, elixir-autogen, elixir-format
+msgid "Linked User"
+msgstr ""
+
+#: lib/mv_web/live/user_live/show.ex:40
+#, elixir-autogen, elixir-format
+msgid "No member linked"
+msgstr ""
+
+#: lib/mv_web/live/member_live/show.ex:51
+#, elixir-autogen, elixir-format
+msgid "No user linked"
+msgstr ""
+
+#: lib/mv_web/live/member_live/show.ex:14
+#: lib/mv_web/live/member_live/show.ex:16
+#, elixir-autogen, elixir-format
+msgid "Back to members list"
+msgstr ""
+
+#: lib/mv_web/live/user_live/show.ex:13
+#: lib/mv_web/live/user_live/show.ex:15
+#, elixir-autogen, elixir-format
+msgid "Back to users list"
+msgstr ""
+
+#: lib/mv_web/components/layouts/navbar.ex:27
+#: lib/mv_web/components/layouts/navbar.ex:33
+#, elixir-autogen, elixir-format
+msgid "Select language"
+msgstr ""
+
+#: lib/mv_web/components/layouts/navbar.ex:40
+#: lib/mv_web/components/layouts/navbar.ex:60
+#, elixir-autogen, elixir-format
+msgid "Toggle dark mode"
+msgstr ""
+
+#: lib/mv_web/live/components/search_bar_component.ex:15
+#: lib/mv_web/live/member_live/index.html.heex:15
+#, elixir-autogen, elixir-format
+msgid "Search..."
+msgstr ""
+
+#: lib/mv_web/components/layouts/navbar.ex:20
+#, elixir-autogen, elixir-format
+msgid "Users"
msgstr ""
diff --git a/priv/gettext/en/LC_MESSAGES/auth.po b/priv/gettext/en/LC_MESSAGES/auth.po
index 1e4e801..21bb4a4 100644
--- a/priv/gettext/en/LC_MESSAGES/auth.po
+++ b/priv/gettext/en/LC_MESSAGES/auth.po
@@ -58,6 +58,3 @@ msgstr ""
msgid "Your password has successfully been reset"
msgstr ""
-
-msgid "Sign in with Rauthy"
-msgstr "Sign in with Vereinscloud"
diff --git a/priv/gettext/en/LC_MESSAGES/default.po b/priv/gettext/en/LC_MESSAGES/default.po
index 2f09378..ac30f5d 100644
--- a/priv/gettext/en/LC_MESSAGES/default.po
+++ b/priv/gettext/en/LC_MESSAGES/default.po
@@ -16,69 +16,69 @@ msgstr ""
msgid "Actions"
msgstr ""
-#: lib/mv_web/live/member_live/index.html.heex:77
+#: lib/mv_web/live/member_live/index.html.heex:84
#: lib/mv_web/live/user_live/index.html.heex:65
#, elixir-autogen, elixir-format
msgid "Are you sure?"
msgstr ""
-#: lib/mv_web/components/layouts.ex:71
-#: lib/mv_web/components/layouts.ex:83
+#: 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:25
-#: lib/mv_web/live/member_live/index.html.heex:62
-#: lib/mv_web/live/member_live/show.ex:36
+#: lib/mv_web/live/member_live/index.html.heex:69
+#: lib/mv_web/live/member_live/show.ex:37
#, elixir-autogen, elixir-format
msgid "City"
msgstr ""
-#: lib/mv_web/live/member_live/index.html.heex:79
+#: lib/mv_web/live/member_live/index.html.heex:86
#: lib/mv_web/live/user_live/index.html.heex:67
#, elixir-autogen, elixir-format
msgid "Delete"
msgstr ""
-#: lib/mv_web/live/member_live/index.html.heex:71
+#: lib/mv_web/live/member_live/index.html.heex:78
#: lib/mv_web/live/user_live/form.ex:109
#: lib/mv_web/live/user_live/index.html.heex:59
#, elixir-autogen, elixir-format
msgid "Edit"
msgstr ""
-#: lib/mv_web/live/member_live/show.ex:18
-#: lib/mv_web/live/member_live/show.ex:81
+#: lib/mv_web/live/member_live/show.ex:19
+#: lib/mv_web/live/member_live/show.ex:95
#, elixir-autogen, elixir-format
msgid "Edit Member"
msgstr ""
#: lib/mv_web/live/member_live/form.ex:18
-#: lib/mv_web/live/member_live/index.html.heex:58
-#: lib/mv_web/live/member_live/show.ex:27
+#: lib/mv_web/live/member_live/index.html.heex:65
+#: lib/mv_web/live/member_live/show.ex:28
#: lib/mv_web/live/user_live/form.ex:14
#: lib/mv_web/live/user_live/index.html.heex:44
-#: lib/mv_web/live/user_live/show.ex:24
+#: lib/mv_web/live/user_live/show.ex:25
#, elixir-autogen, elixir-format
msgid "Email"
msgstr ""
#: lib/mv_web/live/member_live/form.ex:16
-#: lib/mv_web/live/member_live/show.ex:25
+#: lib/mv_web/live/member_live/show.ex:26
#, elixir-autogen, elixir-format
msgid "First Name"
msgstr ""
#: lib/mv_web/live/member_live/form.ex:22
-#: lib/mv_web/live/member_live/index.html.heex:64
-#: lib/mv_web/live/member_live/show.ex:33
+#: lib/mv_web/live/member_live/index.html.heex:71
+#: lib/mv_web/live/member_live/show.ex:34
#, elixir-autogen, elixir-format
msgid "Join Date"
msgstr ""
#: lib/mv_web/live/member_live/form.ex:17
-#: lib/mv_web/live/member_live/show.ex:26
+#: lib/mv_web/live/member_live/show.ex:27
#, elixir-autogen, elixir-format
msgid "Last Name"
msgstr ""
@@ -88,18 +88,18 @@ msgstr ""
msgid "New Member"
msgstr ""
-#: lib/mv_web/live/member_live/index.html.heex:68
+#: lib/mv_web/live/member_live/index.html.heex:75
#: lib/mv_web/live/user_live/index.html.heex:56
#, elixir-autogen, elixir-format
msgid "Show"
msgstr ""
-#: lib/mv_web/components/layouts.ex:78
+#: lib/mv_web/components/layouts.ex:87
#, elixir-autogen, elixir-format
msgid "Something went wrong!"
msgstr ""
-#: lib/mv_web/components/layouts.ex:66
+#: lib/mv_web/components/layouts.ex:75
#, elixir-autogen, elixir-format
msgid "We can't find the internet"
msgstr ""
@@ -110,52 +110,52 @@ msgid "close"
msgstr ""
#: lib/mv_web/live/member_live/form.ex:19
-#: lib/mv_web/live/member_live/show.ex:28
+#: lib/mv_web/live/member_live/show.ex:29
#, elixir-autogen, elixir-format
msgid "Birth Date"
msgstr ""
#: lib/mv_web/live/member_live/form.ex:30
-#: lib/mv_web/live/member_live/show.ex:42
+#: lib/mv_web/live/member_live/show.ex:56
#, elixir-autogen, elixir-format
msgid "Custom Properties"
msgstr ""
#: lib/mv_web/live/member_live/form.ex:23
-#: lib/mv_web/live/member_live/show.ex:34
+#: lib/mv_web/live/member_live/show.ex:35
#, elixir-autogen, elixir-format
msgid "Exit Date"
msgstr ""
#: lib/mv_web/live/member_live/form.ex:27
-#: lib/mv_web/live/member_live/index.html.heex:60
-#: lib/mv_web/live/member_live/show.ex:38
+#: lib/mv_web/live/member_live/index.html.heex:67
+#: lib/mv_web/live/member_live/show.ex:39
#, elixir-autogen, elixir-format
msgid "House Number"
msgstr ""
#: lib/mv_web/live/member_live/form.ex:24
-#: lib/mv_web/live/member_live/show.ex:35
+#: lib/mv_web/live/member_live/show.ex:36
#, elixir-autogen, elixir-format
msgid "Notes"
msgstr ""
#: lib/mv_web/live/member_live/form.ex:20
-#: lib/mv_web/live/member_live/show.ex:29
+#: lib/mv_web/live/member_live/show.ex:30
#, elixir-autogen, elixir-format
msgid "Paid"
msgstr ""
#: lib/mv_web/live/member_live/form.ex:21
-#: lib/mv_web/live/member_live/index.html.heex:63
-#: lib/mv_web/live/member_live/show.ex:32
+#: lib/mv_web/live/member_live/index.html.heex:70
+#: lib/mv_web/live/member_live/show.ex:33
#, elixir-autogen, elixir-format
msgid "Phone Number"
msgstr ""
#: lib/mv_web/live/member_live/form.ex:28
-#: lib/mv_web/live/member_live/index.html.heex:61
-#: lib/mv_web/live/member_live/show.ex:39
+#: lib/mv_web/live/member_live/index.html.heex:68
+#: lib/mv_web/live/member_live/show.ex:40
#, elixir-autogen, elixir-format
msgid "Postal Code"
msgstr ""
@@ -174,8 +174,8 @@ msgid "Saving..."
msgstr ""
#: lib/mv_web/live/member_live/form.ex:26
-#: lib/mv_web/live/member_live/index.html.heex:59
-#: lib/mv_web/live/member_live/show.ex:37
+#: lib/mv_web/live/member_live/index.html.heex:66
+#: lib/mv_web/live/member_live/show.ex:38
#, elixir-autogen, elixir-format
msgid "Street"
msgstr ""
@@ -185,17 +185,17 @@ msgstr ""
msgid "Use this form to manage member records and their properties."
msgstr ""
-#: lib/mv_web/live/member_live/show.ex:24
+#: lib/mv_web/live/member_live/show.ex:25
#, elixir-autogen, elixir-format
msgid "Id"
msgstr ""
-#: lib/mv_web/live/member_live/show.ex:30
+#: lib/mv_web/live/member_live/show.ex:31
#, elixir-autogen, elixir-format
msgid "No"
msgstr ""
-#: lib/mv_web/live/member_live/show.ex:80
+#: lib/mv_web/live/member_live/show.ex:94
#, elixir-autogen, elixir-format, fuzzy
msgid "Show Member"
msgstr ""
@@ -205,7 +205,7 @@ msgstr ""
msgid "This is a member record from your database."
msgstr ""
-#: lib/mv_web/live/member_live/show.ex:30
+#: lib/mv_web/live/member_live/show.ex:31
#, elixir-autogen, elixir-format
msgid "Yes"
msgstr ""
@@ -282,17 +282,17 @@ msgstr ""
msgid "Description"
msgstr ""
-#: lib/mv_web/live/user_live/show.ex:17
+#: lib/mv_web/live/user_live/show.ex:18
#, elixir-autogen, elixir-format, fuzzy
msgid "Edit User"
msgstr ""
-#: lib/mv_web/live/user_live/show.ex:27
+#: lib/mv_web/live/user_live/show.ex:28
#, elixir-autogen, elixir-format
msgid "Enabled"
msgstr ""
-#: lib/mv_web/live/user_live/show.ex:23
+#: lib/mv_web/live/user_live/show.ex:24
#, elixir-autogen, elixir-format
msgid "ID"
msgstr ""
@@ -302,7 +302,7 @@ msgstr ""
msgid "Immutable"
msgstr ""
-#: lib/mv_web/components/layouts/navbar.ex:73
+#: lib/mv_web/components/layouts/navbar.ex:94
#, elixir-autogen, elixir-format
msgid "Logout"
msgstr ""
@@ -318,14 +318,14 @@ msgstr ""
msgid "Member"
msgstr ""
-#: lib/mv_web/components/layouts/navbar.ex:14
-#: lib/mv_web/live/member_live/index.ex:12
+#: lib/mv_web/components/layouts/navbar.ex:19
+#: lib/mv_web/live/member_live/index.ex:14
#: lib/mv_web/live/member_live/index.html.heex:3
#, elixir-autogen, elixir-format
msgid "Members"
msgstr ""
-#: lib/mv_web/live/member_live/index.html.heex:50
+#: lib/mv_web/live/member_live/index.html.heex:57
#: lib/mv_web/live/property_type_live/form.ex:16
#, elixir-autogen, elixir-format
msgid "Name"
@@ -336,12 +336,12 @@ msgstr ""
msgid "New User"
msgstr ""
-#: lib/mv_web/live/user_live/show.ex:27
+#: lib/mv_web/live/user_live/show.ex:28
#, elixir-autogen, elixir-format
msgid "Not enabled"
msgstr ""
-#: lib/mv_web/live/user_live/show.ex:25
+#: lib/mv_web/live/user_live/show.ex:26
#, elixir-autogen, elixir-format, fuzzy
msgid "Not set"
msgstr ""
@@ -353,12 +353,12 @@ msgid "Note"
msgstr ""
#: lib/mv_web/live/user_live/index.html.heex:52
-#: lib/mv_web/live/user_live/show.ex:25
+#: lib/mv_web/live/user_live/show.ex:26
#, elixir-autogen, elixir-format
msgid "OIDC ID"
msgstr ""
-#: lib/mv_web/live/user_live/show.ex:26
+#: lib/mv_web/live/user_live/show.ex:27
#, elixir-autogen, elixir-format
msgid "Password Authentication"
msgstr ""
@@ -368,7 +368,7 @@ msgstr ""
msgid "Please select a property type first"
msgstr ""
-#: lib/mv_web/components/layouts/navbar.ex:69
+#: lib/mv_web/components/layouts/navbar.ex:89
#, elixir-autogen, elixir-format
msgid "Profil"
msgstr ""
@@ -403,17 +403,17 @@ msgstr ""
msgid "Save Property type"
msgstr ""
-#: lib/mv_web/live/member_live/index.html.heex:27
+#: lib/mv_web/live/member_live/index.html.heex:34
#, elixir-autogen, elixir-format
msgid "Select all members"
msgstr ""
-#: lib/mv_web/live/member_live/index.html.heex:41
+#: lib/mv_web/live/member_live/index.html.heex:48
#, elixir-autogen, elixir-format
msgid "Select member"
msgstr ""
-#: lib/mv_web/components/layouts/navbar.ex:72
+#: lib/mv_web/components/layouts/navbar.ex:92
#, elixir-autogen, elixir-format
msgid "Settings"
msgstr ""
@@ -423,7 +423,7 @@ msgstr ""
msgid "Save User"
msgstr ""
-#: lib/mv_web/live/user_live/show.ex:38
+#: lib/mv_web/live/user_live/show.ex:54
#, elixir-autogen, elixir-format, fuzzy
msgid "Show User"
msgstr ""
@@ -554,7 +554,57 @@ msgstr "Set Password"
msgid "User will be created without a password. Check 'Set Password' to add one."
msgstr "User will be created without a password. Check 'Set Password' to add one."
-#: lib/mv_web/auth_overrides.ex:30
-#, elixir-autogen, elixir-format
-msgid "or"
+#: lib/mv_web/live/user_live/show.ex:30
+#, elixir-autogen, elixir-format, fuzzy
+msgid "Linked Member"
+msgstr ""
+
+#: lib/mv_web/live/member_live/show.ex:41
+#, elixir-autogen, elixir-format
+msgid "Linked User"
+msgstr ""
+
+#: lib/mv_web/live/user_live/show.ex:40
+#, elixir-autogen, elixir-format
+msgid "No member linked"
+msgstr ""
+
+#: lib/mv_web/live/member_live/show.ex:51
+#, elixir-autogen, elixir-format
+msgid "No user linked"
+msgstr ""
+
+#: lib/mv_web/live/member_live/show.ex:14
+#: lib/mv_web/live/member_live/show.ex:16
+#, elixir-autogen, elixir-format
+msgid "Back to members list"
+msgstr ""
+
+#: lib/mv_web/live/user_live/show.ex:13
+#: lib/mv_web/live/user_live/show.ex:15
+#, elixir-autogen, elixir-format
+msgid "Back to users list"
+msgstr ""
+
+#: lib/mv_web/components/layouts/navbar.ex:27
+#: lib/mv_web/components/layouts/navbar.ex:33
+#, elixir-autogen, elixir-format, fuzzy
+msgid "Select language"
+msgstr ""
+
+#: lib/mv_web/components/layouts/navbar.ex:40
+#: lib/mv_web/components/layouts/navbar.ex:60
+#, elixir-autogen, elixir-format
+msgid "Toggle dark mode"
+msgstr ""
+
+#: lib/mv_web/live/components/search_bar_component.ex:15
+#: lib/mv_web/live/member_live/index.html.heex:15
+#, elixir-autogen, elixir-format
+msgid "Search..."
+msgstr ""
+
+#: lib/mv_web/components/layouts/navbar.ex:20
+#, elixir-autogen, elixir-format, fuzzy
+msgid "Users"
msgstr ""
diff --git a/priv/gettext/en/LC_MESSAGES/errors.po b/priv/gettext/en/LC_MESSAGES/errors.po
index 844c4f5..62df4a7 100644
--- a/priv/gettext/en/LC_MESSAGES/errors.po
+++ b/priv/gettext/en/LC_MESSAGES/errors.po
@@ -110,3 +110,48 @@ msgstr ""
msgid "must be equal to %{number}"
msgstr ""
+
+## Ash Framework - Standard constraint messages
+msgid "length must be greater than or equal to %{min}"
+msgstr ""
+
+msgid "length must be less than or equal to %{max}"
+msgstr ""
+
+msgid "must be present"
+msgstr ""
+
+## Custom validation messages from Mv.Accounts.User
+msgid "User already has a member. Remove existing member first."
+msgstr ""
+
+msgid "OIDC user_info must contain a non-empty 'sub' or 'id' field"
+msgstr ""
+
+## Custom validation messages from Mv.Membership.Member
+msgid "User is already linked to another member"
+msgstr ""
+
+msgid "User not found"
+msgstr ""
+
+msgid "cannot be in the future"
+msgstr ""
+
+msgid "cannot be before join date"
+msgstr ""
+
+msgid "is not a valid phone number"
+msgstr ""
+
+msgid "must consist of 5 digits"
+msgstr ""
+
+msgid "is not a valid email"
+msgstr ""
+
+msgid "must have length of at least 8"
+msgstr ""
+
+msgid "is required"
+msgstr ""
diff --git a/priv/gettext/errors.pot b/priv/gettext/errors.pot
index eef2de2..8f522c0 100644
--- a/priv/gettext/errors.pot
+++ b/priv/gettext/errors.pot
@@ -107,3 +107,48 @@ msgstr ""
msgid "must be equal to %{number}"
msgstr ""
+
+## Ash Framework - Standard constraint messages
+msgid "length must be greater than or equal to %{min}"
+msgstr ""
+
+msgid "length must be less than or equal to %{max}"
+msgstr ""
+
+msgid "must be present"
+msgstr ""
+
+## Custom validation messages from Mv.Accounts.User
+msgid "User already has a member. Remove existing member first."
+msgstr ""
+
+msgid "OIDC user_info must contain a non-empty 'sub' or 'id' field"
+msgstr ""
+
+## Custom validation messages from Mv.Membership.Member
+msgid "User is already linked to another member"
+msgstr ""
+
+msgid "User not found"
+msgstr ""
+
+msgid "cannot be in the future"
+msgstr ""
+
+msgid "cannot be before join date"
+msgstr ""
+
+msgid "is not a valid phone number"
+msgstr ""
+
+msgid "must consist of 5 digits"
+msgstr ""
+
+msgid "is not a valid email"
+msgstr ""
+
+msgid "must have length of at least 8"
+msgstr ""
+
+msgid "is required"
+msgstr ""
diff --git a/priv/repo/migrations/20250926164519_member_relation.exs b/priv/repo/migrations/20250926164519_member_relation.exs
new file mode 100644
index 0000000..1f63f73
--- /dev/null
+++ b/priv/repo/migrations/20250926164519_member_relation.exs
@@ -0,0 +1,19 @@
+defmodule Mv.Repo.Migrations.MemberRelation 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
+ # Ensure 1:1 relationship - one user can only be linked to one member
+ # This prevents multiple users from sharing the same member account
+ create unique_index(:users, [:member_id], name: "users_unique_member_index")
+ end
+
+ def down do
+ drop_if_exists unique_index(:users, [:member_id], name: "users_unique_member_index")
+ end
+end
diff --git a/priv/repo/migrations/20250926180341_add_unique_email_to_members.exs b/priv/repo/migrations/20250926180341_add_unique_email_to_members.exs
new file mode 100644
index 0000000..a33ce2f
--- /dev/null
+++ b/priv/repo/migrations/20250926180341_add_unique_email_to_members.exs
@@ -0,0 +1,19 @@
+defmodule Mv.Repo.Migrations.AddUniqueEmailToMembers 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
+ # Ensure email uniqueness across all members
+ # This supports upsert operations and prevents duplicate member accounts
+ create unique_index(:members, [:email], name: "members_unique_email_index")
+ end
+
+ def down do
+ drop_if_exists unique_index(:members, [:email], name: "members_unique_email_index")
+ end
+end
diff --git a/priv/repo/migrations/20251016130855_add_constraints_for_user_member_and_property.exs b/priv/repo/migrations/20251016130855_add_constraints_for_user_member_and_property.exs
new file mode 100644
index 0000000..ee13a71
--- /dev/null
+++ b/priv/repo/migrations/20251016130855_add_constraints_for_user_member_and_property.exs
@@ -0,0 +1,46 @@
+defmodule Mv.Repo.Migrations.AddConstraintsForUserMemberAndProperty 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
+ drop constraint(:users, "users_member_id_fkey")
+
+ alter table(:users) do
+ modify :member_id,
+ references(:members,
+ column: :id,
+ name: "users_member_id_fkey",
+ type: :uuid,
+ prefix: "public",
+ on_delete: :nilify_all
+ )
+ end
+
+ create unique_index(:properties, [:member_id, :property_type_id],
+ name: "properties_unique_property_per_member_index"
+ )
+ end
+
+ def down do
+ drop_if_exists unique_index(:properties, [:member_id, :property_type_id],
+ name: "properties_unique_property_per_member_index"
+ )
+
+ drop constraint(:users, "users_member_id_fkey")
+
+ alter table(:users) do
+ modify :member_id,
+ references(:members,
+ column: :id,
+ name: "users_member_id_fkey",
+ type: :uuid,
+ prefix: "public"
+ )
+ end
+ end
+end
diff --git a/priv/repo/seeds.exs b/priv/repo/seeds.exs
index cb38969..d850c7c 100644
--- a/priv/repo/seeds.exs
+++ b/priv/repo/seeds.exs
@@ -48,7 +48,7 @@ Accounts.create_user!(%{email: "admin@mv.local"}, upsert?: true, upsert_identity
|> Ash.Changeset.for_update(:admin_set_password, %{password: "testpassword"})
|> Ash.update!()
-# Create sample members for testing
+# Create sample members for testing - use upsert to prevent duplicates
for member_attrs <- [
%{
first_name: "Hans",
@@ -90,5 +90,96 @@ for member_attrs <- [
house_number: "8"
}
] do
- Membership.create_member!(member_attrs)
+ # Use upsert to prevent duplicates based on email
+ Membership.create_member!(member_attrs, upsert?: true, upsert_identity: :unique_email)
end
+
+# Create additional users for user-member linking examples
+additional_users = [
+ %{email: "hans.mueller@example.de"},
+ %{email: "greta.schmidt@example.de"},
+ %{email: "maria.weber@example.de"},
+ %{email: "thomas.klein@example.de"}
+]
+
+created_users =
+ Enum.map(additional_users, fn user_attrs ->
+ Accounts.create_user!(user_attrs, upsert?: true, upsert_identity: :unique_email)
+ |> Ash.Changeset.for_update(:admin_set_password, %{password: "testpassword"})
+ |> Ash.update!()
+ end)
+
+# Create members with linked users to demonstrate the 1:1 relationship
+# Only create if users don't already have members
+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",
+ city: "Frankfurt",
+ street: "Goetheplatz",
+ house_number: "5",
+ postal_code: "60313",
+ notes: "Linked to user account",
+ # Link to the third user (maria.weber@example.de)
+ user: Enum.at(created_users, 2)
+ },
+ %{
+ 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",
+ city: "Köln",
+ street: "Rheinstraße",
+ house_number: "23",
+ postal_code: "50667",
+ notes: "Linked to user account - needs payment follow-up",
+ # Link to the fourth user (thomas.klein@example.de)
+ user: Enum.at(created_users, 3)
+ }
+]
+
+# Create the linked members - use upsert to prevent duplicates
+Enum.each(linked_members, fn member_attrs ->
+ user = member_attrs.user
+ member_attrs_without_user = Map.delete(member_attrs, :user)
+
+ # Check if user already has a member
+ if user.member_id == nil do
+ # User is free, create member and link - use upsert to prevent duplicates
+ Membership.create_member!(
+ Map.put(member_attrs_without_user, :user, %{id: user.id}),
+ upsert?: true,
+ upsert_identity: :unique_email
+ )
+ else
+ # User already has a member, just create the member without linking - use upsert to prevent duplicates
+ Membership.create_member!(member_attrs_without_user,
+ upsert?: true,
+ upsert_identity: :unique_email
+ )
+ end
+end)
+
+IO.puts("✅ Seeds completed successfully!")
+IO.puts("📝 Created sample data:")
+IO.puts(" - Property types: String, Date, Boolean, Email")
+IO.puts(" - Admin user: admin@mv.local (password: testpassword)")
+IO.puts(" - Sample members: Hans, Greta, Friedrich")
+
+IO.puts(
+ " - Additional users: hans.mueller@example.de, greta.schmidt@example.de, maria.weber@example.de, thomas.klein@example.de"
+)
+
+IO.puts(
+ " - Linked members: Maria Weber ↔ maria.weber@example.de, Thomas Klein ↔ thomas.klein@example.de"
+)
+
+IO.puts("🔗 Visit the application to see user-member relationships in action!")
diff --git a/priv/resource_snapshots/repo/members/20250926180341.json b/priv/resource_snapshots/repo/members/20250926180341.json
new file mode 100644
index 0000000..3582051
--- /dev/null
+++ b/priv/resource_snapshots/repo/members/20250926180341.json
@@ -0,0 +1,202 @@
+{
+ "attributes": [
+ {
+ "allow_nil?": false,
+ "default": "fragment(\"uuid_generate_v7()\")",
+ "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": "first_name",
+ "type": "text"
+ },
+ {
+ "allow_nil?": false,
+ "default": "nil",
+ "generated?": false,
+ "precision": null,
+ "primary_key?": false,
+ "references": null,
+ "scale": null,
+ "size": null,
+ "source": "last_name",
+ "type": "text"
+ },
+ {
+ "allow_nil?": false,
+ "default": "nil",
+ "generated?": false,
+ "precision": null,
+ "primary_key?": false,
+ "references": null,
+ "scale": null,
+ "size": null,
+ "source": "email",
+ "type": "text"
+ },
+ {
+ "allow_nil?": true,
+ "default": "nil",
+ "generated?": false,
+ "precision": null,
+ "primary_key?": false,
+ "references": null,
+ "scale": null,
+ "size": null,
+ "source": "birth_date",
+ "type": "date"
+ },
+ {
+ "allow_nil?": true,
+ "default": "nil",
+ "generated?": false,
+ "precision": null,
+ "primary_key?": false,
+ "references": null,
+ "scale": null,
+ "size": null,
+ "source": "paid",
+ "type": "boolean"
+ },
+ {
+ "allow_nil?": true,
+ "default": "nil",
+ "generated?": false,
+ "precision": null,
+ "primary_key?": false,
+ "references": null,
+ "scale": null,
+ "size": null,
+ "source": "phone_number",
+ "type": "text"
+ },
+ {
+ "allow_nil?": true,
+ "default": "nil",
+ "generated?": false,
+ "precision": null,
+ "primary_key?": false,
+ "references": null,
+ "scale": null,
+ "size": null,
+ "source": "join_date",
+ "type": "date"
+ },
+ {
+ "allow_nil?": true,
+ "default": "nil",
+ "generated?": false,
+ "precision": null,
+ "primary_key?": false,
+ "references": null,
+ "scale": null,
+ "size": null,
+ "source": "exit_date",
+ "type": "date"
+ },
+ {
+ "allow_nil?": true,
+ "default": "nil",
+ "generated?": false,
+ "precision": null,
+ "primary_key?": false,
+ "references": null,
+ "scale": null,
+ "size": null,
+ "source": "notes",
+ "type": "text"
+ },
+ {
+ "allow_nil?": true,
+ "default": "nil",
+ "generated?": false,
+ "precision": null,
+ "primary_key?": false,
+ "references": null,
+ "scale": null,
+ "size": null,
+ "source": "city",
+ "type": "text"
+ },
+ {
+ "allow_nil?": true,
+ "default": "nil",
+ "generated?": false,
+ "precision": null,
+ "primary_key?": false,
+ "references": null,
+ "scale": null,
+ "size": null,
+ "source": "street",
+ "type": "text"
+ },
+ {
+ "allow_nil?": true,
+ "default": "nil",
+ "generated?": false,
+ "precision": null,
+ "primary_key?": false,
+ "references": null,
+ "scale": null,
+ "size": null,
+ "source": "house_number",
+ "type": "text"
+ },
+ {
+ "allow_nil?": true,
+ "default": "nil",
+ "generated?": false,
+ "precision": null,
+ "primary_key?": false,
+ "references": null,
+ "scale": null,
+ "size": null,
+ "source": "postal_code",
+ "type": "text"
+ }
+ ],
+ "base_filter": null,
+ "check_constraints": [],
+ "custom_indexes": [],
+ "custom_statements": [],
+ "has_create_action": true,
+ "hash": "5F070A1E5BEE9883AE864FB5A4A5E81F487A1C57D41576C23BAC8D933005D565",
+ "identities": [
+ {
+ "all_tenants?": false,
+ "base_filter": null,
+ "index_name": "members_unique_email_index",
+ "keys": [
+ {
+ "type": "atom",
+ "value": "email"
+ }
+ ],
+ "name": "unique_email",
+ "nils_distinct?": true,
+ "where": null
+ }
+ ],
+ "multitenancy": {
+ "attribute": null,
+ "global": null,
+ "strategy": null
+ },
+ "repo": "Elixir.Mv.Repo",
+ "schema": null,
+ "table": "members"
+}
\ No newline at end of file
diff --git a/priv/resource_snapshots/repo/members/20251016130855.json b/priv/resource_snapshots/repo/members/20251016130855.json
new file mode 100644
index 0000000..5188002
--- /dev/null
+++ b/priv/resource_snapshots/repo/members/20251016130855.json
@@ -0,0 +1,214 @@
+{
+ "attributes": [
+ {
+ "allow_nil?": false,
+ "default": "fragment(\"uuid_generate_v7()\")",
+ "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": "first_name",
+ "type": "text"
+ },
+ {
+ "allow_nil?": false,
+ "default": "nil",
+ "generated?": false,
+ "precision": null,
+ "primary_key?": false,
+ "references": null,
+ "scale": null,
+ "size": null,
+ "source": "last_name",
+ "type": "text"
+ },
+ {
+ "allow_nil?": false,
+ "default": "nil",
+ "generated?": false,
+ "precision": null,
+ "primary_key?": false,
+ "references": null,
+ "scale": null,
+ "size": null,
+ "source": "email",
+ "type": "text"
+ },
+ {
+ "allow_nil?": true,
+ "default": "nil",
+ "generated?": false,
+ "precision": null,
+ "primary_key?": false,
+ "references": null,
+ "scale": null,
+ "size": null,
+ "source": "birth_date",
+ "type": "date"
+ },
+ {
+ "allow_nil?": true,
+ "default": "nil",
+ "generated?": false,
+ "precision": null,
+ "primary_key?": false,
+ "references": null,
+ "scale": null,
+ "size": null,
+ "source": "paid",
+ "type": "boolean"
+ },
+ {
+ "allow_nil?": true,
+ "default": "nil",
+ "generated?": false,
+ "precision": null,
+ "primary_key?": false,
+ "references": null,
+ "scale": null,
+ "size": null,
+ "source": "phone_number",
+ "type": "text"
+ },
+ {
+ "allow_nil?": true,
+ "default": "nil",
+ "generated?": false,
+ "precision": null,
+ "primary_key?": false,
+ "references": null,
+ "scale": null,
+ "size": null,
+ "source": "join_date",
+ "type": "date"
+ },
+ {
+ "allow_nil?": true,
+ "default": "nil",
+ "generated?": false,
+ "precision": null,
+ "primary_key?": false,
+ "references": null,
+ "scale": null,
+ "size": null,
+ "source": "exit_date",
+ "type": "date"
+ },
+ {
+ "allow_nil?": true,
+ "default": "nil",
+ "generated?": false,
+ "precision": null,
+ "primary_key?": false,
+ "references": null,
+ "scale": null,
+ "size": null,
+ "source": "notes",
+ "type": "text"
+ },
+ {
+ "allow_nil?": true,
+ "default": "nil",
+ "generated?": false,
+ "precision": null,
+ "primary_key?": false,
+ "references": null,
+ "scale": null,
+ "size": null,
+ "source": "city",
+ "type": "text"
+ },
+ {
+ "allow_nil?": true,
+ "default": "nil",
+ "generated?": false,
+ "precision": null,
+ "primary_key?": false,
+ "references": null,
+ "scale": null,
+ "size": null,
+ "source": "street",
+ "type": "text"
+ },
+ {
+ "allow_nil?": true,
+ "default": "nil",
+ "generated?": false,
+ "precision": null,
+ "primary_key?": false,
+ "references": null,
+ "scale": null,
+ "size": null,
+ "source": "house_number",
+ "type": "text"
+ },
+ {
+ "allow_nil?": true,
+ "default": "nil",
+ "generated?": false,
+ "precision": null,
+ "primary_key?": false,
+ "references": null,
+ "scale": null,
+ "size": null,
+ "source": "postal_code",
+ "type": "text"
+ },
+ {
+ "allow_nil?": true,
+ "default": "nil",
+ "generated?": false,
+ "precision": null,
+ "primary_key?": false,
+ "references": null,
+ "scale": null,
+ "size": null,
+ "source": "search_vector",
+ "type": "tsvector"
+ }
+ ],
+ "base_filter": null,
+ "check_constraints": [],
+ "custom_indexes": [],
+ "custom_statements": [],
+ "has_create_action": true,
+ "hash": "6AAC71BCCA5F112087CEF6877A5BBF74EF8965D5DA4812C44CD6E672F882CC3F",
+ "identities": [
+ {
+ "all_tenants?": false,
+ "base_filter": null,
+ "index_name": "members_unique_email_index",
+ "keys": [
+ {
+ "type": "atom",
+ "value": "email"
+ }
+ ],
+ "name": "unique_email",
+ "nils_distinct?": true,
+ "where": null
+ }
+ ],
+ "multitenancy": {
+ "attribute": null,
+ "global": null,
+ "strategy": null
+ },
+ "repo": "Elixir.Mv.Repo",
+ "schema": null,
+ "table": "members"
+}
\ No newline at end of file
diff --git a/priv/resource_snapshots/repo/properties/20251016130855.json b/priv/resource_snapshots/repo/properties/20251016130855.json
new file mode 100644
index 0000000..13958c0
--- /dev/null
+++ b/priv/resource_snapshots/repo/properties/20251016130855.json
@@ -0,0 +1,124 @@
+{
+ "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?": true,
+ "default": "nil",
+ "generated?": false,
+ "precision": null,
+ "primary_key?": false,
+ "references": null,
+ "scale": null,
+ "size": null,
+ "source": "value",
+ "type": "map"
+ },
+ {
+ "allow_nil?": true,
+ "default": "nil",
+ "generated?": false,
+ "precision": null,
+ "primary_key?": false,
+ "references": {
+ "deferrable": false,
+ "destination_attribute": "id",
+ "destination_attribute_default": null,
+ "destination_attribute_generated": null,
+ "index?": false,
+ "match_type": null,
+ "match_with": null,
+ "multitenancy": {
+ "attribute": null,
+ "global": null,
+ "strategy": null
+ },
+ "name": "properties_member_id_fkey",
+ "on_delete": "delete",
+ "on_update": null,
+ "primary_key?": true,
+ "schema": "public",
+ "table": "members"
+ },
+ "scale": null,
+ "size": null,
+ "source": "member_id",
+ "type": "uuid"
+ },
+ {
+ "allow_nil?": true,
+ "default": "nil",
+ "generated?": false,
+ "precision": null,
+ "primary_key?": false,
+ "references": {
+ "deferrable": false,
+ "destination_attribute": "id",
+ "destination_attribute_default": null,
+ "destination_attribute_generated": null,
+ "index?": false,
+ "match_type": null,
+ "match_with": null,
+ "multitenancy": {
+ "attribute": null,
+ "global": null,
+ "strategy": null
+ },
+ "name": "properties_property_type_id_fkey",
+ "on_delete": null,
+ "on_update": null,
+ "primary_key?": true,
+ "schema": "public",
+ "table": "property_types"
+ },
+ "scale": null,
+ "size": null,
+ "source": "property_type_id",
+ "type": "uuid"
+ }
+ ],
+ "base_filter": null,
+ "check_constraints": [],
+ "custom_indexes": [],
+ "custom_statements": [],
+ "has_create_action": true,
+ "hash": "8F90B1AAD1063CF2BB0BDEBBDFBA86AF0B24D854689FB834BC20DFAB2143A451",
+ "identities": [
+ {
+ "all_tenants?": false,
+ "base_filter": null,
+ "index_name": "properties_unique_property_per_member_index",
+ "keys": [
+ {
+ "type": "atom",
+ "value": "member_id"
+ },
+ {
+ "type": "atom",
+ "value": "property_type_id"
+ }
+ ],
+ "name": "unique_property_per_member",
+ "nils_distinct?": true,
+ "where": null
+ }
+ ],
+ "multitenancy": {
+ "attribute": null,
+ "global": null,
+ "strategy": null
+ },
+ "repo": "Elixir.Mv.Repo",
+ "schema": null,
+ "table": "properties"
+}
\ No newline at end of file
diff --git a/priv/resource_snapshots/repo/users/20250926164519.json b/priv/resource_snapshots/repo/users/20250926164519.json
new file mode 100644
index 0000000..7eb68f2
--- /dev/null
+++ b/priv/resource_snapshots/repo/users/20250926164519.json
@@ -0,0 +1,141 @@
+{
+ "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": "email",
+ "type": "citext"
+ },
+ {
+ "allow_nil?": true,
+ "default": "nil",
+ "generated?": false,
+ "precision": null,
+ "primary_key?": false,
+ "references": null,
+ "scale": null,
+ "size": null,
+ "source": "hashed_password",
+ "type": "text"
+ },
+ {
+ "allow_nil?": true,
+ "default": "nil",
+ "generated?": false,
+ "precision": null,
+ "primary_key?": false,
+ "references": null,
+ "scale": null,
+ "size": null,
+ "source": "oidc_id",
+ "type": "text"
+ },
+ {
+ "allow_nil?": true,
+ "default": "nil",
+ "generated?": false,
+ "precision": null,
+ "primary_key?": false,
+ "references": {
+ "deferrable": false,
+ "destination_attribute": "id",
+ "destination_attribute_default": null,
+ "destination_attribute_generated": null,
+ "index?": false,
+ "match_type": null,
+ "match_with": null,
+ "multitenancy": {
+ "attribute": null,
+ "global": null,
+ "strategy": null
+ },
+ "name": "users_member_id_fkey",
+ "on_delete": null,
+ "on_update": null,
+ "primary_key?": true,
+ "schema": "public",
+ "table": "members"
+ },
+ "scale": null,
+ "size": null,
+ "source": "member_id",
+ "type": "uuid"
+ }
+ ],
+ "base_filter": null,
+ "check_constraints": [],
+ "custom_indexes": [],
+ "custom_statements": [],
+ "has_create_action": true,
+ "hash": "FDEBD4840449609DDA8B50D6741C2EEDE9D81DFBC1E26D4BC77DBD9B5A8EA4DC",
+ "identities": [
+ {
+ "all_tenants?": false,
+ "base_filter": null,
+ "index_name": "users_unique_email_index",
+ "keys": [
+ {
+ "type": "atom",
+ "value": "email"
+ }
+ ],
+ "name": "unique_email",
+ "nils_distinct?": true,
+ "where": null
+ },
+ {
+ "all_tenants?": false,
+ "base_filter": null,
+ "index_name": "users_unique_oidc_id_index",
+ "keys": [
+ {
+ "type": "atom",
+ "value": "oidc_id"
+ }
+ ],
+ "name": "unique_oidc_id",
+ "nils_distinct?": true,
+ "where": null
+ },
+ {
+ "all_tenants?": false,
+ "base_filter": null,
+ "index_name": "users_unique_member_index",
+ "keys": [
+ {
+ "type": "atom",
+ "value": "member_id"
+ }
+ ],
+ "name": "unique_member",
+ "nils_distinct?": true,
+ "where": null
+ }
+ ],
+ "multitenancy": {
+ "attribute": null,
+ "global": null,
+ "strategy": null
+ },
+ "repo": "Elixir.Mv.Repo",
+ "schema": null,
+ "table": "users"
+}
\ No newline at end of file
diff --git a/priv/resource_snapshots/repo/users/20251016130855.json b/priv/resource_snapshots/repo/users/20251016130855.json
new file mode 100644
index 0000000..2698fd5
--- /dev/null
+++ b/priv/resource_snapshots/repo/users/20251016130855.json
@@ -0,0 +1,141 @@
+{
+ "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": "email",
+ "type": "citext"
+ },
+ {
+ "allow_nil?": true,
+ "default": "nil",
+ "generated?": false,
+ "precision": null,
+ "primary_key?": false,
+ "references": null,
+ "scale": null,
+ "size": null,
+ "source": "hashed_password",
+ "type": "text"
+ },
+ {
+ "allow_nil?": true,
+ "default": "nil",
+ "generated?": false,
+ "precision": null,
+ "primary_key?": false,
+ "references": null,
+ "scale": null,
+ "size": null,
+ "source": "oidc_id",
+ "type": "text"
+ },
+ {
+ "allow_nil?": true,
+ "default": "nil",
+ "generated?": false,
+ "precision": null,
+ "primary_key?": false,
+ "references": {
+ "deferrable": false,
+ "destination_attribute": "id",
+ "destination_attribute_default": null,
+ "destination_attribute_generated": null,
+ "index?": false,
+ "match_type": null,
+ "match_with": null,
+ "multitenancy": {
+ "attribute": null,
+ "global": null,
+ "strategy": null
+ },
+ "name": "users_member_id_fkey",
+ "on_delete": "nilify",
+ "on_update": null,
+ "primary_key?": true,
+ "schema": "public",
+ "table": "members"
+ },
+ "scale": null,
+ "size": null,
+ "source": "member_id",
+ "type": "uuid"
+ }
+ ],
+ "base_filter": null,
+ "check_constraints": [],
+ "custom_indexes": [],
+ "custom_statements": [],
+ "has_create_action": true,
+ "hash": "1D22936DF847949B543000F3E2E4BDA7D78682AAE6EE0CB9CBD55A4F8F4A7228",
+ "identities": [
+ {
+ "all_tenants?": false,
+ "base_filter": null,
+ "index_name": "users_unique_email_index",
+ "keys": [
+ {
+ "type": "atom",
+ "value": "email"
+ }
+ ],
+ "name": "unique_email",
+ "nils_distinct?": true,
+ "where": null
+ },
+ {
+ "all_tenants?": false,
+ "base_filter": null,
+ "index_name": "users_unique_member_index",
+ "keys": [
+ {
+ "type": "atom",
+ "value": "member_id"
+ }
+ ],
+ "name": "unique_member",
+ "nils_distinct?": true,
+ "where": null
+ },
+ {
+ "all_tenants?": false,
+ "base_filter": null,
+ "index_name": "users_unique_oidc_id_index",
+ "keys": [
+ {
+ "type": "atom",
+ "value": "oidc_id"
+ }
+ ],
+ "name": "unique_oidc_id",
+ "nils_distinct?": true,
+ "where": null
+ }
+ ],
+ "multitenancy": {
+ "attribute": null,
+ "global": null,
+ "strategy": null
+ },
+ "repo": "Elixir.Mv.Repo",
+ "schema": null,
+ "table": "users"
+}
\ No newline at end of file
diff --git a/test/accounts/user_member_deletion_test.exs b/test/accounts/user_member_deletion_test.exs
new file mode 100644
index 0000000..52a3865
--- /dev/null
+++ b/test/accounts/user_member_deletion_test.exs
@@ -0,0 +1,88 @@
+defmodule Mv.Accounts.UserMemberDeletionTest do
+ @moduledoc """
+ Tests for ON DELETE SET NULL constraint on users.member_id.
+ When a member is deleted, the linked user should remain but with member_id set to NULL.
+ """
+ use Mv.DataCase, async: true
+ alias Mv.Accounts
+ alias Mv.Membership
+
+ describe "User remains when linked Member is deleted (ON DELETE SET NULL)" do
+ @valid_user_attrs %{
+ email: "test@example.com"
+ }
+
+ @valid_member_attrs %{
+ first_name: "John",
+ last_name: "Doe",
+ email: "john@example.com"
+ }
+
+ test "deleting a member sets the user's member_id to NULL" do
+ # Create a member
+ {:ok, member} = Membership.create_member(@valid_member_attrs)
+
+ # Create a user linked to the member
+ {:ok, user} =
+ Accounts.create_user(Map.put(@valid_user_attrs, :member, %{id: member.id}))
+
+ # Verify the relationship is established
+ {:ok, user_before_delete} = Ash.get(Mv.Accounts.User, user.id, load: [:member])
+ assert user_before_delete.member_id == member.id
+ assert user_before_delete.member.id == member.id
+
+ # Delete the member
+ :ok = Membership.destroy_member(member)
+
+ # Verify the user still exists but member_id is NULL
+ {:ok, user_after_delete} = Ash.get(Mv.Accounts.User, user.id, load: [:member])
+ assert user_after_delete.id == user.id
+ assert user_after_delete.member_id == nil
+ assert user_after_delete.member == nil
+ end
+
+ test "user can be linked to a new member after old member is deleted" do
+ # Create first member
+ {:ok, member1} = Membership.create_member(@valid_member_attrs)
+
+ # Create user linked to first member
+ {:ok, user} =
+ Accounts.create_user(Map.put(@valid_user_attrs, :member, %{id: member1.id}))
+
+ assert user.member_id == member1.id
+
+ # Delete first member
+ :ok = Membership.destroy_member(member1)
+
+ # Reload user from database to get updated member_id (should be NULL)
+ {:ok, user_after_delete} = Ash.get(Mv.Accounts.User, user.id)
+ assert user_after_delete.member_id == nil
+
+ # Create second member
+ {:ok, member2} =
+ Membership.create_member(%{
+ first_name: "Jane",
+ last_name: "Smith",
+ email: "jane@example.com"
+ })
+
+ # Link user to second member (use reloaded user)
+ {:ok, updated_user} = Accounts.update_user(user_after_delete, %{member: %{id: member2.id}})
+
+ # Verify new relationship
+ {:ok, final_user} = Ash.get(Mv.Accounts.User, updated_user.id, load: [:member])
+ assert final_user.member_id == member2.id
+ assert final_user.member.id == member2.id
+ end
+
+ test "member without linked user can be deleted normally" do
+ {:ok, member} = Membership.create_member(@valid_member_attrs)
+
+ # Delete member (no users linked)
+ assert :ok = Membership.destroy_member(member)
+
+ # Verify member is deleted
+ assert {:error, _} = Ash.get(Mv.Membership.Member, member.id)
+ end
+ end
+end
diff --git a/test/accounts/user_member_relationship_test.exs b/test/accounts/user_member_relationship_test.exs
new file mode 100644
index 0000000..b64f5ec
--- /dev/null
+++ b/test/accounts/user_member_relationship_test.exs
@@ -0,0 +1,197 @@
+defmodule Mv.Accounts.UserMemberRelationshipTest do
+ # Using async: true for faster test execution
+ # This is safe because all database operations are sandboxed per test
+ use Mv.DataCase, async: true
+ alias Mv.Accounts
+ alias Mv.Membership
+
+ describe "User-Member Relationship - Basic Tests" do
+ @valid_user_attrs %{
+ email: "test@example.com"
+ }
+
+ @valid_member_attrs %{
+ first_name: "John",
+ last_name: "Doe",
+ email: "john@example.com"
+ }
+
+ test "user can exist without member" do
+ {:ok, user} = Accounts.create_user(@valid_user_attrs)
+ assert user.member_id == nil
+
+ # Load the relationship to test it
+ {:ok, user_with_member} = Ash.get(Mv.Accounts.User, user.id, load: [:member])
+ assert user_with_member.member == nil
+ end
+
+ test "member can exist without user" do
+ {:ok, member} = Membership.create_member(@valid_member_attrs)
+ assert member.id != nil
+ assert member.first_name == "John"
+
+ # Load the relationship to test it
+ {:ok, member_with_user} = Ash.get(Mv.Membership.Member, member.id, load: [:user])
+ assert member_with_user.user == nil
+ end
+ end
+
+ describe "User-Member Relationship - Linking Tests" do
+ @valid_user_attrs %{
+ email: "test1@example.com"
+ }
+
+ @valid_member_attrs %{
+ first_name: "Alice",
+ last_name: "Johnson",
+ email: "alice@example.com"
+ }
+
+ test "user can be linked to member during user creation" do
+ {:ok, member} = Membership.create_member(@valid_member_attrs)
+
+ user_attrs = Map.put(@valid_user_attrs, :member, %{id: member.id})
+ {:ok, user} = Accounts.create_user(user_attrs)
+
+ # Load the relationship to test it
+ {:ok, user_with_member} = Ash.get(Mv.Accounts.User, user.id, load: [:member])
+ assert user_with_member.member.id == member.id
+ end
+
+ test "member can be linked to user during member creation using manage_relationship" do
+ {:ok, user} = Accounts.create_user(@valid_user_attrs)
+
+ member_attrs = Map.put(@valid_member_attrs, :user, %{id: user.id})
+ {:ok, member} = Membership.create_member(member_attrs)
+
+ # Load the relationship to test it
+ {:ok, member_with_user} = Ash.get(Mv.Membership.Member, member.id, load: [:user])
+ assert member_with_user.user.id == user.id
+ end
+
+ test "user can be linked to member during update" do
+ {:ok, user} = Accounts.create_user(@valid_user_attrs)
+ {:ok, member} = Membership.create_member(@valid_member_attrs)
+
+ {:ok, updated_user} = Accounts.update_user(user, %{member: %{id: member.id}})
+
+ # Load the relationship to test it
+ {:ok, user_with_member} = Ash.get(Mv.Accounts.User, updated_user.id, load: [:member])
+ assert user_with_member.member.id == member.id
+ end
+
+ test "member can be linked to user during update using manage_relationship" do
+ {:ok, user} = Accounts.create_user(@valid_user_attrs)
+ {:ok, member} = Membership.create_member(@valid_member_attrs)
+
+ {:ok, _updated_member} = Membership.update_member(member, %{user: %{id: user.id}})
+
+ # Load the relationship to test it
+ {:ok, member_with_user} = Ash.get(Mv.Membership.Member, member.id, load: [:user])
+ assert member_with_user.user.id == user.id
+ end
+ end
+
+ describe "User-Member Relationship - Inverse Relationship Tests" do
+ @valid_user_attrs %{
+ email: "test2@example.com"
+ }
+
+ @valid_member_attrs %{
+ first_name: "Bob",
+ last_name: "Smith",
+ email: "bob@example.com"
+ }
+
+ test "ash resolves inverse relationship automatically" do
+ {:ok, member} = Membership.create_member(@valid_member_attrs)
+
+ user_attrs = Map.put(@valid_user_attrs, :member, %{id: member.id})
+ {:ok, user} = Accounts.create_user(user_attrs)
+
+ # Load relationships
+ {:ok, user_with_member} = Ash.get(Mv.Accounts.User, user.id, load: [:member])
+ {:ok, member_with_user} = Ash.get(Mv.Membership.Member, member.id, load: [:user])
+
+ assert user_with_member.member.id == member.id
+ assert member_with_user.user.id == user.id
+ end
+
+ test "member can find associated user" do
+ {:ok, member} = Membership.create_member(@valid_member_attrs)
+
+ {:ok, user} = Accounts.create_user(%{email: "test3@example.com", member: %{id: member.id}})
+ {:ok, member_with_user} = Ash.get(Mv.Membership.Member, member.id, load: [:user])
+ assert member_with_user.user.id == user.id
+ end
+ end
+
+ describe "User-Member Relationship - Preventing Duplicates" do
+ @valid_user_attrs %{
+ email: "test4@example.com"
+ }
+
+ @valid_member_attrs %{
+ first_name: "Charlie",
+ last_name: "Brown",
+ email: "charlie@example.com"
+ }
+
+ test "prevents overwriting a member of already linked user on update" do
+ {:ok, existing_member} = Membership.create_member(@valid_member_attrs)
+
+ user_attrs = Map.put(@valid_user_attrs, :member, %{id: existing_member.id})
+ {:ok, user} = Accounts.create_user(user_attrs)
+
+ {:ok, member2} =
+ Membership.create_member(%{
+ first_name: "Dave",
+ last_name: "Wilson",
+ email: "dave@example.com"
+ })
+
+ assert {:error, %Ash.Error.Invalid{}} =
+ Accounts.update_user(user, %{member: %{id: member2.id}})
+ end
+
+ test "prevents linking user to already linked member on update" do
+ {:ok, existing_user} = Accounts.create_user(@valid_user_attrs)
+ {:ok, member} = Membership.create_member(@valid_member_attrs)
+
+ {:ok, _updated_user} = Accounts.update_user(existing_user, %{member: %{id: member.id}})
+
+ {:ok, user2} = Accounts.create_user(%{email: "test5@example.com"})
+
+ assert {:error, %Ash.Error.Invalid{}} =
+ Accounts.update_user(user2, %{member: %{id: member.id}})
+ end
+
+ test "prevents linking member to already linked user on creation" do
+ {:ok, existing_member} = Membership.create_member(@valid_member_attrs)
+
+ user_attrs = Map.put(@valid_user_attrs, :member, %{id: existing_member.id})
+ {:ok, user} = Accounts.create_user(user_attrs)
+
+ assert {:error, %Ash.Error.Invalid{}} =
+ Membership.create_member(%{
+ first_name: "Dave",
+ last_name: "Wilson",
+ email: "dave@example.com",
+ user: %{id: user.id}
+ })
+ end
+
+ test "prevents linking user to already linked member on creation" do
+ {:ok, existing_user} = Accounts.create_user(@valid_user_attrs)
+ {:ok, member} = Membership.create_member(@valid_member_attrs)
+
+ {:ok, _updated_user} = Accounts.update_user(existing_user, %{member: %{id: member.id}})
+
+ assert {:error, %Ash.Error.Invalid{}} =
+ Accounts.create_user(%{
+ email: "test5@example.com",
+ member: %{id: member.id}
+ })
+ end
+ end
+end
diff --git a/test/mv_web/user_live/form_test.exs b/test/mv_web/user_live/form_test.exs
index decc789..111ff42 100644
--- a/test/mv_web/user_live/form_test.exs
+++ b/test/mv_web/user_live/form_test.exs
@@ -255,7 +255,7 @@ defmodule MvWeb.UserLive.FormTest do
conn = Plug.Test.init_test_session(conn, locale: "de")
{:ok, _view, html} = live(conn, "/users/new")
- assert html =~ "Neuer Benutzer"
+ assert html =~ "Neue*r Benutzer*in"
assert html =~ "E-Mail"
assert html =~ "Passwort setzen"
end
diff --git a/test/mv_web/user_live/index_test.exs b/test/mv_web/user_live/index_test.exs
index bb78377..6393e3b 100644
--- a/test/mv_web/user_live/index_test.exs
+++ b/test/mv_web/user_live/index_test.exs
@@ -7,7 +7,7 @@ defmodule MvWeb.UserLive.IndexTest do
conn = conn_with_oidc_user(conn)
conn = Plug.Test.init_test_session(conn, locale: "de")
{:ok, _view, html} = live(conn, "/users")
- assert html =~ "Benutzer auflisten"
+ assert html =~ "Benutzer*innen auflisten"
end
test "shows translated title in English", %{conn: conn} do
@@ -362,8 +362,8 @@ defmodule MvWeb.UserLive.IndexTest do
conn = Plug.Test.init_test_session(conn, locale: "de")
{:ok, _view, html} = live(conn, "/users")
- assert html =~ "Alle Benutzer auswählen"
- assert html =~ "Benutzer auswählen"
+ assert html =~ "Alle Benutzer*innen auswählen"
+ assert html =~ "Benutzer*in auswählen"
end
test "shows English translations for selection", %{conn: conn} do