WIP feat: member user relation

This commit is contained in:
Moritz 2025-07-24 20:15:01 +02:00
parent 997691746a
commit 4e6f5a517a
Signed by: moritz
GPG key ID: 1020A035E5DD0824
35 changed files with 1208 additions and 192 deletions

View file

@ -4,7 +4,7 @@ defmodule MvWeb.MemberLive.Form do
@impl true
def render(assigns) do
~H"""
<Layouts.app flash={@flash}>
<Layouts.app flash={@flash} current_user={@current_user}>
<.header>
{@page_title}
<:subtitle>
@ -155,7 +155,7 @@ defmodule MvWeb.MemberLive.Form do
AshPhoenix.Form.for_update(
member,
:update_member,
api: Mv.Membership,
domain: Mv.Membership,
as: "member",
params: params,
forms: [auto?: true]
@ -172,7 +172,7 @@ defmodule MvWeb.MemberLive.Form do
AshPhoenix.Form.for_create(
Mv.Membership.Member,
:create_member,
api: Mv.Membership,
domain: Mv.Membership,
as: "member",
params: %{"properties" => socket.assigns[:initial_properties]},
forms: [auto?: true]

View file

@ -1,4 +1,4 @@
<Layouts.app flash={@flash}>
<Layouts.app flash={@flash} current_user={@current_user}>
<.header>
{gettext("Members")}
<:actions>

View file

@ -5,7 +5,7 @@ defmodule MvWeb.MemberLive.Show do
@impl true
def render(assigns) do
~H"""
<Layouts.app flash={@flash}>
<Layouts.app flash={@flash} current_user={@current_user}>
<.header>
{@member.first_name} {@member.last_name}
<:subtitle>{gettext("This is a member record from your database.")}</:subtitle>

View file

@ -4,7 +4,7 @@ defmodule MvWeb.PropertyLive.Form do
@impl true
def render(assigns) do
~H"""
<Layouts.app flash={@flash}>
<Layouts.app flash={@flash} current_user={@current_user}>
<.header>
{@page_title}
<:subtitle>{gettext("Use this form to manage property records in your database.")}</:subtitle>

View file

@ -4,7 +4,7 @@ defmodule MvWeb.PropertyLive.Index do
@impl true
def render(assigns) do
~H"""
<Layouts.app flash={@flash}>
<Layouts.app flash={@flash} current_user={@current_user}>
<.header>
Listing Properties
<:actions>

View file

@ -4,7 +4,7 @@ defmodule MvWeb.PropertyLive.Show do
@impl true
def render(assigns) do
~H"""
<Layouts.app flash={@flash}>
<Layouts.app flash={@flash} current_user={@current_user}>
<.header>
Property {@property.id}
<:subtitle>This is a property record from your database.</:subtitle>

View file

@ -4,7 +4,7 @@ defmodule MvWeb.PropertyTypeLive.Form do
@impl true
def render(assigns) do
~H"""
<Layouts.app flash={@flash}>
<Layouts.app flash={@flash} current_user={@current_user}>
<.header>
{@page_title}
<:subtitle>

View file

@ -4,7 +4,7 @@ defmodule MvWeb.PropertyTypeLive.Index do
@impl true
def render(assigns) do
~H"""
<Layouts.app flash={@flash}>
<Layouts.app flash={@flash} current_user={@current_user}>
<.header>
Listing Property types
<:actions>

View file

@ -4,7 +4,7 @@ defmodule MvWeb.PropertyTypeLive.Show do
@impl true
def render(assigns) do
~H"""
<Layouts.app flash={@flash}>
<Layouts.app flash={@flash} current_user={@current_user}>
<.header>
Property type {@property_type.id}
<:subtitle>This is a property_type record from your database.</:subtitle>

View file

@ -4,7 +4,7 @@ defmodule MvWeb.UserLive.Form do
@impl true
def render(assigns) do
~H"""
<Layouts.app flash={@flash}>
<Layouts.app flash={@flash} current_user={@current_user}>
<.header>
{@page_title}
<:subtitle>{gettext("Use this form to manage user records in your database.")}</:subtitle>
@ -13,6 +13,53 @@ defmodule MvWeb.UserLive.Form do
<.form for={@form} id="user-form" phx-change="validate" phx-submit="save">
<.input field={@form[:email]} label={gettext("Email")} required type="email" />
<!-- Member Assignment Section -->
<div class="mt-6 space-y-4">
<h3 class="text-lg font-medium">{gettext("Member Assignment")}</h3>
<label class="flex items-center space-x-2">
<input
type="radio"
name="member_assignment_mode"
value="create_new"
phx-click="set_member_mode"
phx-value-mode="create_new"
checked={@member_assignment_mode == "create_new"}
class="radio radio-sm"
/>
<span class="text-sm">
{gettext("Create new member automatically")}
</span>
</label>
<label class="flex items-center space-x-2">
<input
type="radio"
name="member_assignment_mode"
value="assign_existing"
phx-click="set_member_mode"
phx-value-mode="assign_existing"
checked={@member_assignment_mode == "assign_existing"}
class="radio radio-sm"
/>
<span class="text-sm">
{gettext("Assign to existing member")}
</span>
</label>
<%= if @member_assignment_mode == "assign_existing" do %>
<div class="ml-6 mt-2">
<.input
field={@form[:member_id]}
label={gettext("Select Member")}
type="select"
options={@available_members}
prompt={gettext("Choose a member...")}
/>
</div>
<% end %>
</div>
<!-- Password Section -->
<div class="mt-6">
<label class="flex items-center space-x-2">
@ -109,12 +156,25 @@ defmodule MvWeb.UserLive.Form do
action = if is_nil(user), do: gettext("New"), else: gettext("Edit")
page_title = action <> " " <> gettext("User")
# Load available members that have no user assigned
{:ok, available_members} = Mv.Membership.list_members()
available_members_with_user = Ash.load!(available_members, :user)
available_member_options =
available_members_with_user
|> Enum.filter(fn member -> is_nil(member.user) end)
|> Enum.map(fn member ->
{"#{member.first_name} #{member.last_name} (#{member.email})", member.id}
end)
{:ok,
socket
|> assign(:return_to, return_to(params["return_to"]))
|> assign(user: user)
|> assign(:page_title, page_title)
|> assign(:show_password_fields, false)
|> assign(:member_assignment_mode, "create_new")
|> assign(:available_members, available_member_options)
|> assign_form()}
end
@ -133,6 +193,15 @@ defmodule MvWeb.UserLive.Form do
{:noreply, socket}
end
def handle_event("set_member_mode", %{"mode" => mode}, socket) do
socket =
socket
|> assign(:member_assignment_mode, mode)
|> assign_form()
{:noreply, socket}
end
def handle_event("validate", %{"user" => user_params}, socket) do
{:noreply, assign(socket, form: AshPhoenix.Form.validate(socket.assigns.form, user_params))}
end
@ -161,14 +230,30 @@ defmodule MvWeb.UserLive.Form do
if user do
# For existing users, use admin password action if password fields are shown
action = if show_password_fields, do: :admin_set_password, else: :update_user
AshPhoenix.Form.for_update(user, action, domain: Mv.Accounts, as: "user")
AshPhoenix.Form.for_update(user, action,
as: "user",
actor: socket.assigns.current_user,
domain: Mv.Accounts
)
else
# For new users, use password registration if password fields are shown
action = if show_password_fields, do: :register_with_password, else: :create_user
# Only include member_id if assign_existing mode is selected AND not using password action
accept =
if socket.assigns.member_assignment_mode == "assign_existing" and
not show_password_fields do
[:email, :member_id]
else
[:email]
end
AshPhoenix.Form.for_create(Mv.Accounts.User, action,
as: "user",
actor: socket.assigns.current_user,
domain: Mv.Accounts,
as: "user"
accept: accept
)
end

View file

@ -4,7 +4,10 @@ defmodule MvWeb.UserLive.Index do
@impl true
def mount(_params, _session, socket) do
users = Ash.read!(Mv.Accounts.User, domain: Mv.Accounts)
users =
Ash.read!(Mv.Accounts.User, domain: Mv.Accounts)
|> Ash.load!(:member)
sorted = Enum.sort_by(users, & &1.email)
{:ok,

View file

@ -1,4 +1,4 @@
<Layouts.app flash={@flash}>
<Layouts.app flash={@flash} current_user={@current_user}>
<.header>
{gettext("Listing Users")}
<:actions>
@ -49,6 +49,11 @@
>
{user.email}
</:col>
<:col :let={user} label={gettext("Member")}>
{if user.member,
do: "#{user.member.first_name} #{user.member.last_name}",
else: gettext("No member")}
</:col>
<:col :let={user} label={gettext("OIDC ID")}>{user.oidc_id}</:col>
<:action :let={user}>

View file

@ -4,7 +4,7 @@ defmodule MvWeb.UserLive.Show do
@impl true
def render(assigns) do
~H"""
<Layouts.app flash={@flash}>
<Layouts.app flash={@flash} current_user={@current_user}>
<.header>
{gettext("User")} {@user.email}
<:subtitle>{gettext("This is a user record from your database.")}</:subtitle>
@ -22,6 +22,14 @@ defmodule MvWeb.UserLive.Show do
<.list>
<:item title={gettext("ID")}>{@user.id}</:item>
<:item title={gettext("Email")}>{@user.email}</:item>
<:item title={gettext("Member")}>
<div :if={@user.member}>
<.link navigate={~p"/members/#{@user.member.id}"} class="link link-primary">
{@user.member.first_name} {@user.member.last_name} ({@user.member.email})
</.link>
</div>
<span :if={!@user.member}>{gettext("No member assigned")}</span>
</:item>
<:item title={gettext("OIDC ID")}>{@user.oidc_id || gettext("Not set")}</:item>
<:item title={gettext("Password Authentication")}>
{if @user.hashed_password, do: gettext("Enabled"), else: gettext("Not enabled")}
@ -33,9 +41,13 @@ defmodule MvWeb.UserLive.Show do
@impl true
def mount(%{"id" => id}, _session, socket) do
user =
Ash.get!(Mv.Accounts.User, id, domain: Mv.Accounts)
|> Ash.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