diff --git a/lib/accounts/user.ex b/lib/accounts/user.ex
index b085407..c65b882 100644
--- a/lib/accounts/user.ex
+++ b/lib/accounts/user.ex
@@ -60,15 +60,54 @@ defmodule Mv.Accounts.User do
end
actions do
- defaults [:read, :create, :destroy, :update]
+ defaults [:read, :create, :destroy]
+
+ update :update do
+ primary? true
+ require_atomic? false
+ end
create :create_user do
+ # 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
+ # 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 function cannot be done atomically
+ 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 +115,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})
@@ -125,6 +165,28 @@ defmodule Mv.Accounts.User do
validate string_length(:password, min: 8) do
where action_is([:register_with_password, :admin_set_password])
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
def validate_oidc_id_present(changeset, _context) do
@@ -146,12 +208,16 @@ defmodule Mv.Accounts.User do
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 583f173..7b898a8 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)],
@@ -170,5 +237,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/mv_web/components/layouts/navbar.ex b/lib/mv_web/components/layouts/navbar.ex
index b917ddc..5efaa1b 100644
--- a/lib/mv_web/components/layouts/navbar.ex
+++ b/lib/mv_web/components/layouts/navbar.ex
@@ -18,13 +18,20 @@ 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 304709c..838a960 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 3bf6baf..3735f8f 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/mix.lock b/mix.lock
index 46c9f3f..607508a 100644
--- a/mix.lock
+++ b/mix.lock
@@ -1,5 +1,5 @@
%{
- "ash": {:hex, :ash, "3.5.34", "e79e82dc3e3e66fb54a598eeba5feca2d1c3af6a0e752a3378cbad8d7a47dc6f", [:mix], [{:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:ecto, "~> 3.7", [hex: :ecto, repo: "hexpm", optional: false]}, {:ets, "~> 0.8", [hex: :ets, repo: "hexpm", optional: false]}, {:igniter, ">= 0.6.4 and < 1.0.0-0", [hex: :igniter, repo: "hexpm", optional: true]}, {:jason, ">= 1.0.0", [hex: :jason, repo: "hexpm", optional: false]}, {:picosat_elixir, "~> 0.2", [hex: :picosat_elixir, repo: "hexpm", optional: true]}, {:plug, ">= 0.0.0", [hex: :plug, repo: "hexpm", optional: true]}, {:reactor, "~> 0.11", [hex: :reactor, repo: "hexpm", optional: false]}, {:simple_sat, ">= 0.1.1 and < 1.0.0-0", [hex: :simple_sat, repo: "hexpm", optional: true]}, {:spark, ">= 2.2.65 and < 3.0.0-0", [hex: :spark, repo: "hexpm", optional: false]}, {:splode, ">= 0.2.6 and < 1.0.0-0", [hex: :splode, repo: "hexpm", optional: false]}, {:stream_data, "~> 1.0", [hex: :stream_data, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.1", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "5cbf0a4d0ec1b6525b0782e4f5509c55dad446d657c635ceffe55f78a59132cd"},
+ "ash": {:hex, :ash, "3.5.43", "222f9a8ac26ad3b029f8e69306cc83091c992d858b4538af12e33a148f301cab", [:mix], [{:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:ecto, "~> 3.7", [hex: :ecto, repo: "hexpm", optional: false]}, {:ets, "~> 0.8", [hex: :ets, repo: "hexpm", optional: false]}, {:igniter, ">= 0.6.29 and < 1.0.0-0", [hex: :igniter, repo: "hexpm", optional: true]}, {:jason, ">= 1.0.0", [hex: :jason, repo: "hexpm", optional: false]}, {:picosat_elixir, "~> 0.2", [hex: :picosat_elixir, repo: "hexpm", optional: true]}, {:plug, ">= 0.0.0", [hex: :plug, repo: "hexpm", optional: true]}, {:reactor, "~> 0.11", [hex: :reactor, repo: "hexpm", optional: false]}, {:simple_sat, ">= 0.1.1 and < 1.0.0-0", [hex: :simple_sat, repo: "hexpm", optional: true]}, {:spark, ">= 2.3.3 and < 3.0.0-0", [hex: :spark, repo: "hexpm", optional: false]}, {:splode, ">= 0.2.6 and < 1.0.0-0", [hex: :splode, repo: "hexpm", optional: false]}, {:stream_data, "~> 1.0", [hex: :stream_data, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.1", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "48b2aa274c524f5b968c563dd56aec8f9b278c529c8aa46e6fe0ca564c26cc1c"},
"ash_admin": {:hex, :ash_admin, "0.13.16", "6b30487e88b0a47b2da1c508b157be6d86b954ba464a01d412e6d5e047a53ad5", [:mix], [{:ash, ">= 3.4.63 and < 4.0.0-0", [hex: :ash, repo: "hexpm", optional: false]}, {:ash_phoenix, ">= 2.1.8 and < 3.0.0-0", [hex: :ash_phoenix, repo: "hexpm", optional: false]}, {:gettext, "~> 0.26", [hex: :gettext, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.7", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 4.1", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:phoenix_live_view, "~> 1.1-rc", [hex: :phoenix_live_view, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: false]}], "hexpm", "07a03d761b2029d8b1fefad815eb3cc525532ae9d440e7ca3f5c9f4c1ecb5d17"},
"ash_authentication": {:hex, :ash_authentication, "4.9.9", "23ec61bedc3157c258ece622c6f0f6a7645df275ff5e794d513cc6e8798471eb", [:mix], [{:argon2_elixir, "~> 4.0", [hex: :argon2_elixir, repo: "hexpm", optional: true]}, {:ash, ">= 3.4.29 and < 4.0.0-0", [hex: :ash, repo: "hexpm", optional: false]}, {:ash_postgres, ">= 2.6.8 and < 3.0.0-0", [hex: :ash_postgres, repo: "hexpm", optional: true]}, {:assent, "~> 0.2.13", [hex: :assent, repo: "hexpm", optional: false]}, {:bcrypt_elixir, "~> 3.0", [hex: :bcrypt_elixir, repo: "hexpm", optional: false]}, {:castore, "~> 1.0", [hex: :castore, repo: "hexpm", optional: false]}, {:finch, "~> 0.19", [hex: :finch, repo: "hexpm", optional: false]}, {:igniter, "~> 0.4", [hex: :igniter, repo: "hexpm", optional: true]}, {:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}, {:joken, "~> 2.5", [hex: :joken, repo: "hexpm", optional: false]}, {:plug, "~> 1.13", [hex: :plug, repo: "hexpm", optional: false]}, {:spark, "~> 2.0", [hex: :spark, repo: "hexpm", optional: false]}, {:splode, "~> 0.2", [hex: :splode, repo: "hexpm", optional: false]}], "hexpm", "ab8bd1277ff570425346dcf22dd14a059d9bbce0c28d24964b60e51fabaddda8"},
"ash_authentication_phoenix": {:hex, :ash_authentication_phoenix, "2.10.5", "9f3b1bee4a57f2269efea61e5efe55472683429b8a5bf1ebdd02d9748640f106", [:mix], [{:ash, "~> 3.0", [hex: :ash, repo: "hexpm", optional: false]}, {:ash_authentication, ">= 4.9.1 and < 5.0.0-0", [hex: :ash_authentication, repo: "hexpm", optional: false]}, {:ash_phoenix, ">= 2.3.11 and < 3.0.0-0", [hex: :ash_phoenix, repo: "hexpm", optional: false]}, {:bcrypt_elixir, "~> 3.0", [hex: :bcrypt_elixir, repo: "hexpm", optional: false]}, {:gettext, "~> 0.26", [hex: :gettext, repo: "hexpm", optional: true]}, {:igniter, ">= 0.5.25 and < 1.0.0-0", [hex: :igniter, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.6", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:phoenix_html_helpers, "~> 1.0", [hex: :phoenix_html_helpers, repo: "hexpm", optional: false]}, {:phoenix_live_view, "~> 1.0", [hex: :phoenix_live_view, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: false]}, {:slugify, "~> 1.3", [hex: :slugify, repo: "hexpm", optional: false]}], "hexpm", "3f25778d126c7e759444df0855077802c93299457afdf26566f8de6320ba56da"},
@@ -18,7 +18,7 @@
"db_connection": {:hex, :db_connection, "2.8.0", "64fd82cfa6d8e25ec6660cea73e92a4cbc6a18b31343910427b702838c4b33b2", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "008399dae5eee1bf5caa6e86d204dcb44242c82b1ed5e22c881f2c34da201b15"},
"decimal": {:hex, :decimal, "2.3.0", "3ad6255aa77b4a3c4f818171b12d237500e63525c2fd056699967a3e7ea20f62", [:mix], [], "hexpm", "a4d66355cb29cb47c3cf30e71329e58361cfcb37c34235ef3bf1d7bf3773aeac"},
"dns_cluster": {:hex, :dns_cluster, "0.2.0", "aa8eb46e3bd0326bd67b84790c561733b25c5ba2fe3c7e36f28e88f384ebcb33", [:mix], [], "hexpm", "ba6f1893411c69c01b9e8e8f772062535a4cf70f3f35bcc964a324078d8c8240"},
- "ecto": {:hex, :ecto, "3.13.2", "7d0c0863f3fc8d71d17fc3ad3b9424beae13f02712ad84191a826c7169484f01", [:mix], [{:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "669d9291370513ff56e7b7e7081b7af3283d02e046cf3d403053c557894a0b3e"},
+ "ecto": {:hex, :ecto, "3.13.3", "6a983f0917f8bdc7a89e96f2bf013f220503a0da5d8623224ba987515b3f0d80", [:mix], [{:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "1927db768f53a88843ff25b6ba7946599a8ca8a055f69ad8058a1432a399af94"},
"ecto_commons": {:hex, :ecto_commons, "0.3.6", "7b1d9e59396cf8c8cbe5a26d50d03f9b6d0fe6c640210dd503622f276f1e59bb", [:mix], [{:burnex, "~> 3.0", [hex: :burnex, repo: "hexpm", optional: true]}, {:ecto, "~> 3.4", [hex: :ecto, repo: "hexpm", optional: false]}, {:ex_phone_number, "~> 0.2", [hex: :ex_phone_number, repo: "hexpm", optional: false]}, {:luhn, "~> 0.3.0", [hex: :luhn, repo: "hexpm", optional: false]}], "hexpm", "3f12981a1e398f206c5d2014e7b732b7ec91b110b9cb84875cb5b28fc75d7a0a"},
"ecto_sql": {:hex, :ecto_sql, "3.13.2", "a07d2461d84107b3d037097c822ffdd36ed69d1cf7c0f70e12a3d1decf04e2e1", [:mix], [{:db_connection, "~> 2.4.1 or ~> 2.5", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.13.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.7", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.19 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:tds, "~> 2.1.1 or ~> 2.2", [hex: :tds, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "539274ab0ecf1a0078a6a72ef3465629e4d6018a3028095dc90f60a19c371717"},
"elixir_make": {:hex, :elixir_make, "0.9.0", "6484b3cd8c0cee58f09f05ecaf1a140a8c97670671a6a0e7ab4dc326c3109726", [:mix], [], "hexpm", "db23d4fd8b757462ad02f8aa73431a426fe6671c80b200d9710caf3d1dd0ffdb"},
@@ -33,7 +33,7 @@
"glob_ex": {:hex, :glob_ex, "0.1.11", "cb50d3f1ef53f6ca04d6252c7fde09fd7a1cf63387714fe96f340a1349e62c93", [:mix], [], "hexpm", "342729363056e3145e61766b416769984c329e4378f1d558b63e341020525de4"},
"heroicons": {:git, "https://github.com/tailwindlabs/heroicons.git", "0435d4ca364a608cc75e2f8683d374e55abbae26", [tag: "v2.2.0", sparse: "optimized", depth: 1]},
"hpax": {:hex, :hpax, "1.0.3", "ed67ef51ad4df91e75cc6a1494f851850c0bd98ebc0be6e81b026e765ee535aa", [:mix], [], "hexpm", "8eab6e1cfa8d5918c2ce4ba43588e894af35dbd8e91e6e55c817bca5847df34a"},
- "igniter": {:hex, :igniter, "0.6.27", "a7c01062db56f5c5ac0f36ff8ef3cce1d61cd6bf59e50c52f4a38dc926aa9728", [:mix], [{:glob_ex, "~> 0.1.7", [hex: :glob_ex, repo: "hexpm", optional: false]}, {:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}, {:owl, "~> 0.11", [hex: :owl, repo: "hexpm", optional: false]}, {:phx_new, "~> 1.7", [hex: :phx_new, repo: "hexpm", optional: true]}, {:req, "~> 0.5", [hex: :req, repo: "hexpm", optional: false]}, {:rewrite, ">= 1.1.1 and < 2.0.0-0", [hex: :rewrite, repo: "hexpm", optional: false]}, {:sourceror, "~> 1.4", [hex: :sourceror, repo: "hexpm", optional: false]}, {:spitfire, ">= 0.1.3 and < 1.0.0-0", [hex: :spitfire, repo: "hexpm", optional: false]}], "hexpm", "d1eda5271932dcb9f00f94936c3dc12a2b96466f895f4b3fb82a0caada6d6447"},
+ "igniter": {:hex, :igniter, "0.6.30", "83a466369ebb8fe009e0823c7bf04314dc545122c2d48f896172fc79df33e99d", [:mix], [{:glob_ex, "~> 0.1.7", [hex: :glob_ex, repo: "hexpm", optional: false]}, {:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}, {:owl, "~> 0.11", [hex: :owl, repo: "hexpm", optional: false]}, {:phx_new, "~> 1.7", [hex: :phx_new, repo: "hexpm", optional: true]}, {:req, "~> 0.5", [hex: :req, repo: "hexpm", optional: false]}, {:rewrite, ">= 1.1.1 and < 2.0.0-0", [hex: :rewrite, repo: "hexpm", optional: false]}, {:sourceror, "~> 1.4", [hex: :sourceror, repo: "hexpm", optional: false]}, {:spitfire, ">= 0.1.3 and < 1.0.0-0", [hex: :spitfire, repo: "hexpm", optional: false]}], "hexpm", "76a14d5b7f850bb03b5243088c3649d54a2e52e34a2aa1104dee23cf50a8bae0"},
"iterex": {:hex, :iterex, "0.1.2", "58f9b9b9a22a55cbfc7b5234a9c9c63eaac26d276b3db80936c0e1c60355a5a6", [:mix], [], "hexpm", "2e103b8bcc81757a9af121f6dc0df312c9a17220f302b1193ef720460d03029d"},
"jason": {:hex, :jason, "1.4.4", "b9226785a9aa77b6857ca22832cffa5d5011a667207eb2a0ad56adb5db443b8a", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "c5eb0cab91f094599f94d55bc63409236a8ec69a21a67814529e8d5f6cc90b3b"},
"joken": {:hex, :joken, "2.6.2", "5daaf82259ca603af4f0b065475099ada1b2b849ff140ccd37f4b6828ca6892a", [:mix], [{:jose, "~> 1.11.10", [hex: :jose, repo: "hexpm", optional: false]}], "hexpm", "5134b5b0a6e37494e46dbf9e4dad53808e5e787904b7c73972651b51cce3d72b"},
@@ -47,7 +47,7 @@
"mix_audit": {:hex, :mix_audit, "2.1.5", "c0f77cee6b4ef9d97e37772359a187a166c7a1e0e08b50edf5bf6959dfe5a016", [:make, :mix], [{:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}, {:yaml_elixir, "~> 2.11", [hex: :yaml_elixir, repo: "hexpm", optional: false]}], "hexpm", "87f9298e21da32f697af535475860dc1d3617a010e0b418d2ec6142bc8b42d69"},
"nimble_options": {:hex, :nimble_options, "1.1.1", "e3a492d54d85fc3fd7c5baf411d9d2852922f66e69476317787a7b2bb000a61b", [:mix], [], "hexpm", "821b2470ca9442c4b6984882fe9bb0389371b8ddec4d45a9504f00a66f650b44"},
"nimble_pool": {:hex, :nimble_pool, "1.1.0", "bf9c29fbdcba3564a8b800d1eeb5a3c58f36e1e11d7b7fb2e084a643f645f06b", [:mix], [], "hexpm", "af2e4e6b34197db81f7aad230c1118eac993acc0dae6bc83bac0126d4ae0813a"},
- "owl": {:hex, :owl, "0.12.2", "65906b525e5c3ef51bab6cba7687152be017aebe1da077bb719a5ee9f7e60762", [:mix], [{:ucwidth, "~> 0.2", [hex: :ucwidth, repo: "hexpm", optional: true]}], "hexpm", "6398efa9e1fea70a04d24231e10dcd66c1ac1aa2da418d20ef5357ec61de2880"},
+ "owl": {:hex, :owl, "0.13.0", "26010e066d5992774268f3163506972ddac0a7e77bfe57fa42a250f24d6b876e", [:mix], [{:ucwidth, "~> 0.2", [hex: :ucwidth, repo: "hexpm", optional: true]}], "hexpm", "59bf9d11ce37a4db98f57cb68fbfd61593bf419ec4ed302852b6683d3d2f7475"},
"phoenix": {:hex, :phoenix, "1.8.0", "dc5d256bb253110266ded8c4a6a167e24fabde2e14b8e474d262840ae8d8ea18", [:mix], [{:bandit, "~> 1.0", [hex: :bandit, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.1", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.7", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:plug_crypto, "~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:websock_adapter, "~> 0.5.3", [hex: :websock_adapter, repo: "hexpm", optional: false]}], "hexpm", "15f6e9cb76646ad8d9f2947240519666fc2c4f29f8a93ad9c7664916ab4c167b"},
"phoenix_ecto": {:hex, :phoenix_ecto, "4.6.5", "c4ef322acd15a574a8b1a08eff0ee0a85e73096b53ce1403b6563709f15e1cea", [:mix], [{:ecto, "~> 3.5", [hex: :ecto, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.1", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:plug, "~> 1.9", [hex: :plug, repo: "hexpm", optional: false]}, {:postgrex, "~> 0.16 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}], "hexpm", "26ec3208eef407f31b748cadd044045c6fd485fbff168e35963d2f9dfff28d4b"},
"phoenix_html": {:hex, :phoenix_html, "4.2.1", "35279e2a39140068fc03f8874408d58eef734e488fc142153f055c5454fd1c08", [:mix], [], "hexpm", "cff108100ae2715dd959ae8f2a8cef8e20b593f8dfd031c9cba92702cf23e053"},
@@ -61,13 +61,13 @@
"plug": {:hex, :plug, "1.18.1", "5067f26f7745b7e31bc3368bc1a2b818b9779faa959b49c934c17730efc911cf", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "57a57db70df2b422b564437d2d33cf8d33cd16339c1edb190cd11b1a3a546cc2"},
"plug_crypto": {:hex, :plug_crypto, "2.1.1", "19bda8184399cb24afa10be734f84a16ea0a2bc65054e23a62bb10f06bc89491", [:mix], [], "hexpm", "6470bce6ffe41c8bd497612ffde1a7e4af67f36a15eea5f921af71cf3e11247c"},
"postgrex": {:hex, :postgrex, "0.21.1", "2c5cc830ec11e7a0067dd4d623c049b3ef807e9507a424985b8dcf921224cd88", [:mix], [{:db_connection, "~> 2.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.5 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:table, "~> 0.1.0", [hex: :table, repo: "hexpm", optional: true]}], "hexpm", "27d8d21c103c3cc68851b533ff99eef353e6a0ff98dc444ea751de43eb48bdac"},
- "reactor": {:hex, :reactor, "0.15.6", "d717f9add549b25a089a94c90197718d2d838e35d81dd776b1d81587d4cf2aaa", [:mix], [{:igniter, "~> 0.4", [hex: :igniter, repo: "hexpm", optional: true]}, {:iterex, "~> 0.1", [hex: :iterex, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:libgraph, "~> 0.16", [hex: :libgraph, repo: "hexpm", optional: false]}, {:spark, "~> 2.0", [hex: :spark, repo: "hexpm", optional: false]}, {:splode, "~> 0.2", [hex: :splode, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.2", [hex: :telemetry, repo: "hexpm", optional: false]}, {:yaml_elixir, "~> 2.11", [hex: :yaml_elixir, repo: "hexpm", optional: false]}, {:ymlr, "~> 5.0", [hex: :ymlr, repo: "hexpm", optional: false]}], "hexpm", "74db98165e3644d86e0f723672d91ceca4339eaa935bcad7e78bf146a46d77b9"},
+ "reactor": {:hex, :reactor, "0.16.0", "394087fe0f01b09e5cbcbf6525d9a54cd484582214e0e9e59f69ebc8d79eb70c", [:mix], [{:igniter, "~> 0.4", [hex: :igniter, repo: "hexpm", optional: true]}, {:iterex, "~> 0.1", [hex: :iterex, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:libgraph, "~> 0.16", [hex: :libgraph, repo: "hexpm", optional: false]}, {:spark, "~> 2.0", [hex: :spark, repo: "hexpm", optional: false]}, {:splode, "~> 0.2", [hex: :splode, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.2", [hex: :telemetry, repo: "hexpm", optional: false]}, {:yaml_elixir, "~> 2.11", [hex: :yaml_elixir, repo: "hexpm", optional: false]}, {:ymlr, "~> 5.0", [hex: :ymlr, repo: "hexpm", optional: false]}], "hexpm", "9ac43e70a9a36c5a016b02b6c068933dfd36edc0e3abd9cd6325a30194900c66"},
"req": {:hex, :req, "0.5.15", "662020efb6ea60b9f0e0fac9be88cd7558b53fe51155a2d9899de594f9906ba9", [:mix], [{:brotli, "~> 0.3.1", [hex: :brotli, repo: "hexpm", optional: true]}, {:ezstd, "~> 1.0", [hex: :ezstd, repo: "hexpm", optional: true]}, {:finch, "~> 0.17", [hex: :finch, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mime, "~> 2.0.6 or ~> 2.1", [hex: :mime, repo: "hexpm", optional: false]}, {:nimble_csv, "~> 1.0", [hex: :nimble_csv, repo: "hexpm", optional: true]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "a6513a35fad65467893ced9785457e91693352c70b58bbc045b47e5eb2ef0c53"},
"rewrite": {:hex, :rewrite, "1.1.2", "f5a5d10f5fed1491a6ff48e078d4585882695962ccc9e6c779bae025d1f92eda", [:mix], [{:glob_ex, "~> 0.1", [hex: :glob_ex, repo: "hexpm", optional: false]}, {:sourceror, "~> 1.0", [hex: :sourceror, repo: "hexpm", optional: false]}, {:text_diff, "~> 0.1", [hex: :text_diff, repo: "hexpm", optional: false]}], "hexpm", "7f8b94b1e3528d0a47b3e8b7bfeca559d2948a65fa7418a9ad7d7712703d39d4"},
"slugify": {:hex, :slugify, "1.3.1", "0d3b8b7e5c1eeaa960e44dce94382bee34a39b3ea239293e457a9c5b47cc6fd3", [:mix], [], "hexpm", "cb090bbeb056b312da3125e681d98933a360a70d327820e4b7f91645c4d8be76"},
"sobelow": {:hex, :sobelow, "0.14.0", "dd82aae8f72503f924fe9dd97ffe4ca694d2f17ec463dcfd365987c9752af6ee", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "7ecf91e298acfd9b24f5d761f19e8f6e6ac585b9387fb6301023f1f2cd5eed5f"},
"sourceror": {:hex, :sourceror, "1.10.0", "38397dedbbc286966ec48c7af13e228b171332be1ad731974438c77791945ce9", [:mix], [], "hexpm", "29dbdfc92e04569c9d8e6efdc422fc1d815f4bd0055dc7c51b8800fb75c4b3f1"},
- "spark": {:hex, :spark, "2.2.68", "4c4547c88d73311e3157bc402ab27f3a7bbd5e55a2ee92bcf7cd3a0a475d201e", [:mix], [{:igniter, ">= 0.3.64 and < 1.0.0-0", [hex: :igniter, repo: "hexpm", optional: true]}, {:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: true]}, {:sourceror, "~> 1.2", [hex: :sourceror, repo: "hexpm", optional: true]}], "hexpm", "2fa4dd89801415a098a421589633f0e3df7ed9ff4046e80a65d35a413bc0d194"},
+ "spark": {:hex, :spark, "2.3.5", "f30d30ecc3b4ab9b932d9aada66af7677fc1f297a2c349b0bcec3eafb9f996e8", [:mix], [{:igniter, ">= 0.3.64 and < 1.0.0-0", [hex: :igniter, repo: "hexpm", optional: true]}, {:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: true]}, {:sourceror, "~> 1.2", [hex: :sourceror, repo: "hexpm", optional: true]}], "hexpm", "0e9d339704d5d148f77f2b2fef3bcfc873a9e9bb4224fcf289c545d65827202f"},
"spitfire": {:hex, :spitfire, "0.2.1", "29e154873f05444669c7453d3d931820822cbca5170e88f0f8faa1de74a79b47", [:mix], [], "hexpm", "6eeed75054a38341b2e1814d41bb0a250564092358de2669fdb57ff88141d91b"},
"splode": {:hex, :splode, "0.2.9", "3a2776e187c82f42f5226b33b1220ccbff74f4bcc523dd4039c804caaa3ffdc7", [:mix], [], "hexpm", "8002b00c6e24f8bd1bcced3fbaa5c33346048047bb7e13d2f3ad428babbd95c3"},
"stream_data": {:hex, :stream_data, "1.2.0", "58dd3f9e88afe27dc38bef26fce0c84a9e7a96772b2925c7b32cd2435697a52b", [:mix], [], "hexpm", "eb5c546ee3466920314643edf68943a5b14b32d1da9fe01698dc92b73f89a9ed"},
@@ -83,6 +83,6 @@
"websock": {:hex, :websock, "0.5.3", "2f69a6ebe810328555b6fe5c831a851f485e303a7c8ce6c5f675abeb20ebdadc", [:mix], [], "hexpm", "6105453d7fac22c712ad66fab1d45abdf049868f253cf719b625151460b8b453"},
"websock_adapter": {:hex, :websock_adapter, "0.5.8", "3b97dc94e407e2d1fc666b2fb9acf6be81a1798a2602294aac000260a7c4a47d", [:mix], [{:bandit, ">= 0.6.0", [hex: :bandit, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.6", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "315b9a1865552212b5f35140ad194e67ce31af45bcee443d4ecb96b5fd3f3782"},
"yamerl": {:hex, :yamerl, "0.10.0", "4ff81fee2f1f6a46f1700c0d880b24d193ddb74bd14ef42cb0bcf46e81ef2f8e", [:rebar3], [], "hexpm", "346adb2963f1051dc837a2364e4acf6eb7d80097c0f53cbdc3046ec8ec4b4e6e"},
- "yaml_elixir": {:hex, :yaml_elixir, "2.11.0", "9e9ccd134e861c66b84825a3542a1c22ba33f338d82c07282f4f1f52d847bd50", [:mix], [{:yamerl, "~> 0.10", [hex: :yamerl, repo: "hexpm", optional: false]}], "hexpm", "53cc28357ee7eb952344995787f4bb8cc3cecbf189652236e9b163e8ce1bc242"},
+ "yaml_elixir": {:hex, :yaml_elixir, "2.12.0", "30343ff5018637a64b1b7de1ed2a3ca03bc641410c1f311a4dbdc1ffbbf449c7", [:mix], [{:yamerl, "~> 0.10", [hex: :yamerl, repo: "hexpm", optional: false]}], "hexpm", "ca6bacae7bac917a7155dca0ab6149088aa7bc800c94d0fe18c5238f53b313c6"},
"ymlr": {:hex, :ymlr, "5.1.4", "b924d61e1fc1ec371cde6ab3ccd9311110b1e052fc5c2460fb322e8380e7712a", [:mix], [], "hexpm", "75f16cf0709fbd911b30311a0359a7aa4b5476346c01882addefd5f2b1cfaa51"},
}
diff --git a/priv/gettext/de/LC_MESSAGES/auth.po b/priv/gettext/de/LC_MESSAGES/auth.po
index 0f2202d..b794f37 100644
--- a/priv/gettext/de/LC_MESSAGES/auth.po
+++ b/priv/gettext/de/LC_MESSAGES/auth.po
@@ -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..92d254a 100644
--- a/priv/gettext/de/LC_MESSAGES/default.po
+++ b/priv/gettext/de/LC_MESSAGES/default.po
@@ -29,7 +29,7 @@ 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/show.ex:37
#, elixir-autogen, elixir-format
msgid "City"
msgstr "Stadt"
@@ -47,37 +47,37 @@ msgstr "Löschen"
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/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/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"
@@ -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/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/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/show.ex:40
#, elixir-autogen, elixir-format
msgid "Postal Code"
msgstr "Postleitzahl"
@@ -174,7 +174,7 @@ 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/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"
-#: 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:87
#, elixir-autogen, elixir-format
msgid "Logout"
msgstr "Abmelden"
@@ -335,12 +335,12 @@ msgstr "Name"
msgid "New User"
msgstr "Neuer Benutzer"
-#: 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:83
#, 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
@@ -412,7 +412,7 @@ msgstr "Alle Mitglieder auswählen"
msgid "Select member"
msgstr "Mitglied auswählen"
-#: lib/mv_web/components/layouts/navbar.ex:72
+#: lib/mv_web/components/layouts/navbar.ex:86
#, elixir-autogen, elixir-format
msgid "Settings"
msgstr "Einstellungen"
@@ -422,7 +422,7 @@ msgstr "Einstellungen"
msgid "Save User"
msgstr "Benutzer 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"
@@ -438,14 +438,14 @@ 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
@@ -553,7 +553,46 @@ msgstr "Passwort setzen"
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."
-#: 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üpfter Benutzer"
+
+#: 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 "Kein Benutzer 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 Benutzerliste"
+
+#: lib/mv_web/components/layouts/navbar.ex:21
+#: lib/mv_web/components/layouts/navbar.ex:27
+#, elixir-autogen, elixir-format
+msgid "Select language"
+msgstr "Sprache auswählen"
+
+#: lib/mv_web/components/layouts/navbar.ex:34
+#: lib/mv_web/components/layouts/navbar.ex:54
+#, elixir-autogen, elixir-format
+msgid "Toggle dark mode"
+msgstr "Dunklen Modus umschalten"
diff --git a/priv/gettext/de/LC_MESSAGES/errors.po b/priv/gettext/de/LC_MESSAGES/errors.po
index 844c4f5..b9332ab 100644
--- a/priv/gettext/de/LC_MESSAGES/errors.po
+++ b/priv/gettext/de/LC_MESSAGES/errors.po
@@ -12,101 +12,101 @@ 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"
diff --git a/priv/gettext/default.pot b/priv/gettext/default.pot
index a9bfb08..1262c00 100644
--- a/priv/gettext/default.pot
+++ b/priv/gettext/default.pot
@@ -30,7 +30,7 @@ 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/show.ex:37
#, elixir-autogen, elixir-format
msgid "City"
msgstr ""
@@ -48,37 +48,37 @@ msgstr ""
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/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/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 ""
@@ -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/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/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/show.ex:40
#, elixir-autogen, elixir-format
msgid "Postal Code"
msgstr ""
@@ -175,7 +175,7 @@ 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/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:87
#, elixir-autogen, elixir-format
msgid "Logout"
msgstr ""
@@ -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:83
#, elixir-autogen, elixir-format
msgid "Profil"
msgstr ""
@@ -413,7 +413,7 @@ msgstr ""
msgid "Select member"
msgstr ""
-#: lib/mv_web/components/layouts/navbar.ex:72
+#: lib/mv_web/components/layouts/navbar.ex:86
#, 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,46 @@ 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:21
+#: lib/mv_web/components/layouts/navbar.ex:27
+#, elixir-autogen, elixir-format
+msgid "Select language"
+msgstr ""
+
+#: lib/mv_web/components/layouts/navbar.ex:34
+#: lib/mv_web/components/layouts/navbar.ex:54
+#, elixir-autogen, elixir-format
+msgid "Toggle dark mode"
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..933d6b8 100644
--- a/priv/gettext/en/LC_MESSAGES/default.po
+++ b/priv/gettext/en/LC_MESSAGES/default.po
@@ -30,7 +30,7 @@ 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/show.ex:37
#, elixir-autogen, elixir-format
msgid "City"
msgstr ""
@@ -48,37 +48,37 @@ msgstr ""
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/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/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 ""
@@ -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/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/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/show.ex:40
#, elixir-autogen, elixir-format
msgid "Postal Code"
msgstr ""
@@ -175,7 +175,7 @@ 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/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:87
#, elixir-autogen, elixir-format
msgid "Logout"
msgstr ""
@@ -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:83
#, elixir-autogen, elixir-format
msgid "Profil"
msgstr ""
@@ -413,7 +413,7 @@ msgstr ""
msgid "Select member"
msgstr ""
-#: lib/mv_web/components/layouts/navbar.ex:72
+#: lib/mv_web/components/layouts/navbar.ex:86
#, 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,46 @@ 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:21
+#: lib/mv_web/components/layouts/navbar.ex:27
+#, elixir-autogen, elixir-format, fuzzy
+msgid "Select language"
+msgstr ""
+
+#: lib/mv_web/components/layouts/navbar.ex:34
+#: lib/mv_web/components/layouts/navbar.ex:54
+#, elixir-autogen, elixir-format
+msgid "Toggle dark mode"
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..daaa24c
--- /dev/null
+++ b/priv/repo/migrations/20250926164519_member_relation.exs
@@ -0,0 +1,17 @@
+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
+ 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..51b874f
--- /dev/null
+++ b/priv/repo/migrations/20250926180341_add_unique_email_to_members.exs
@@ -0,0 +1,17 @@
+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
+ 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/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/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/test/accounts/user_member_relationship_test.exs b/test/accounts/user_member_relationship_test.exs
new file mode 100644
index 0000000..19cbe62
--- /dev/null
+++ b/test/accounts/user_member_relationship_test.exs
@@ -0,0 +1,195 @@
+defmodule Mv.Accounts.UserMemberRelationshipTest do
+ use Mv.DataCase, async: false
+ 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