WIP: feature/119_user_as_member #124
3 changed files with 307 additions and 69 deletions
|
|
@ -66,10 +66,15 @@ defmodule Mv.Accounts.User do
|
|||
|
||||
create :create_user do
|
||||
accept [:email, :member_id]
|
||||
argument :member, :map
|
||||
change manage_relationship(:member, type: :create)
|
||||
|
|
||||
end
|
||||
|
||||
update :update_user do
|
||||
require_atomic? false
|
||||
accept [:email, :member_id]
|
||||
argument :member, :map
|
||||
change manage_relationship(:member, on_match: :update, on_no_match: :create)
|
||||
end
|
||||
|
||||
# Admin action for direct password changes in admin panel
|
||||
|
|
@ -118,6 +123,16 @@ defmodule Mv.Accounts.User do
|
|||
|> Ash.Changeset.change_attribute(:oidc_id, user_info["sub"] || user_info["id"])
|
||||
end
|
||||
end
|
||||
|
||||
create :register_with_password do
|
||||
accept [:email]
|
||||
argument :password, :string, allow_nil?: false, sensitive?: true
|
||||
argument :password_confirmation, :string, allow_nil?: false, sensitive?: true
|
||||
argument :member, :map
|
||||
change AshAuthentication.Strategy.Password.HashPasswordChange
|
||||
change AshAuthentication.GenerateTokenChange
|
||||
change manage_relationship(:member, type: :create)
|
||||
end
|
||||
end
|
||||
|
||||
# Global validations - applied to all relevant actions
|
||||
|
|
|
|||
50
lib/mv_web/components/member_form_component.ex
Normal file
50
lib/mv_web/components/member_form_component.ex
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
defmodule MvWeb.MemberFormComponent do
|
||||
|
rafael
commented
This is pretty similar to the This is pretty similar to the `MemberLive.Form` liveview, can we extract some common code here?
|
||||
use Phoenix.Component
|
||||
use Gettext, backend: MvWeb.Gettext
|
||||
import MvWeb.CoreComponents
|
||||
|
||||
@doc """
|
||||
Reusable form for member data (without <form>-tag and buttons).
|
||||
Expects:
|
||||
- form: Phoenix.HTML.FormField for Member
|
||||
- property_types: List of PropertyTypes
|
||||
"""
|
||||
attr :form, :any, required: true
|
||||
attr :property_types, :list, required: true
|
||||
def member_form(assigns) do
|
||||
~H"""
|
||||
<.input field={@form[:first_name]} label={gettext("First Name")} required />
|
||||
<.input field={@form[:last_name]} label={gettext("Last Name")} required />
|
||||
<.input field={@form[:email]} label={gettext("Email")} required type="email" />
|
||||
<.input field={@form[:birth_date]} label={gettext("Birth Date")} type="date" />
|
||||
<.input field={@form[:paid]} label={gettext("Paid")} type="checkbox" />
|
||||
<.input field={@form[:phone_number]} label={gettext("Phone Number")} />
|
||||
<.input field={@form[:join_date]} label={gettext("Join Date")} type="date" />
|
||||
<.input field={@form[:exit_date]} label={gettext("Exit Date")} type="date" />
|
||||
<.input field={@form[:notes]} label={gettext("Notes")} />
|
||||
<.input field={@form[:city]} label={gettext("City")} />
|
||||
<.input field={@form[:street]} label={gettext("Street")} />
|
||||
<.input field={@form[:house_number]} label={gettext("House Number")} />
|
||||
<.input field={@form[:postal_code]} label={gettext("Postal Code")} />
|
||||
|
||||
<h3 class="mt-8 mb-2 text-lg font-semibold">{gettext("Custom Properties")}</h3>
|
||||
<.inputs_for :let={f_property} field={@form[:properties]}>
|
||||
<% type = Enum.find(@property_types, &(&1.id == f_property[:property_type_id].value)) %>
|
||||
<.inputs_for :let={value_form} field={f_property[:value]}>
|
||||
<% input_type =
|
||||
cond do
|
||||
type && type.value_type == :boolean -> "checkbox"
|
||||
type && type.value_type == :date -> :date
|
||||
true -> "text"
|
||||
end %>
|
||||
<.input field={value_form[:value]} label={type && type.name} type={input_type} />
|
||||
</.inputs_for>
|
||||
<input
|
||||
type="hidden"
|
||||
name={f_property[:property_type_id].name}
|
||||
value={f_property[:property_type_id].value}
|
||||
/>
|
||||
</.inputs_for>
|
||||
"""
|
||||
end
|
||||
end
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
defmodule MvWeb.UserLive.Form do
|
||||
use MvWeb, :live_view
|
||||
import MvWeb.MemberFormComponent, only: [member_form: 1]
|
||||
|
||||
@impl true
|
||||
def render(assigns) do
|
||||
|
|
@ -14,51 +15,82 @@ defmodule MvWeb.UserLive.Form do
|
|||
<.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>
|
||||
<%= if !@user || !@user.member do %>
|
||||
<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...")}
|
||||
<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"
|
||||
/>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
<span class="text-sm">
|
||||
{gettext("Create new member with custom data")}
|
||||
</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 %>
|
||||
|
||||
<%= if @member_assignment_mode == "create_new" do %>
|
||||
<div class="ml-6 mt-4 p-4 bg-gray-50 rounded-lg">
|
||||
<%= if @form[:member] do %>
|
||||
<.inputs_for :let={f_member} field={@form[:member]}>
|
||||
<.member_form form={f_member} property_types={@property_types} />
|
||||
</.inputs_for>
|
||||
<% else %>
|
||||
<p class="text-sm text-gray-600">{gettext("Member form will be available after saving the user.")}</p>
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
<% else %>
|
||||
<div class="mt-6 space-y-4">
|
||||
<h3 class="text-lg font-medium">{gettext("Member Assignment")}</h3>
|
||||
<p class="text-sm text-gray-600">
|
||||
{gettext("Edit assigned member")}: {@user.member.first_name} {@user.member.last_name} ({@user.member.email})
|
||||
</p>
|
||||
<%= if @member_form do %>
|
||||
<div class="ml-6 mt-4 p-4 bg-gray-50 rounded-lg">
|
||||
<.form for={@member_form} id="member-form" phx-change="validate_member" phx-submit="save_member">
|
||||
<.member_form form={@member_form} property_types={@property_types} />
|
||||
|
carla
commented
It only worked for me, if you use here It only worked for me, if you use here `inputs_for` as above. Only one submit button for the form is working. So save_member would never be triggered. You can remove the "Save member" button here and adjust the "Save" event to handle both - saving user and member. That worked for me.
|
||||
<.button type="submit" variant="primary" class="mt-4">
|
||||
{gettext("Save Member")}
|
||||
</.button>
|
||||
</.form>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<!-- Password Section -->
|
||||
<div class="mt-6">
|
||||
|
|
@ -150,7 +182,16 @@ defmodule MvWeb.UserLive.Form do
|
|||
user =
|
||||
case params["id"] do
|
||||
nil -> nil
|
||||
id -> Ash.get!(Mv.Accounts.User, id, domain: Mv.Accounts)
|
||||
id ->
|
||||
user = Ash.get!(Mv.Accounts.User, id, domain: Mv.Accounts)
|
||||
|> Ash.load!(:member)
|
||||
|
||||
if user.member do
|
||||
{:ok, member_with_properties} = Ash.load(user.member, properties: [:property_type])
|
||||
%{user | member: member_with_properties}
|
||||
else
|
||||
user
|
||||
end
|
||||
end
|
||||
|
||||
action = if is_nil(user), do: gettext("New"), else: gettext("Edit")
|
||||
|
|
@ -167,6 +208,75 @@ defmodule MvWeb.UserLive.Form do
|
|||
{"#{member.first_name} #{member.last_name} (#{member.email})", member.id}
|
||||
end)
|
||||
|
||||
# Load PropertyTypes for MemberForm
|
||||
{:ok, property_types} = Mv.Membership.list_property_types()
|
||||
|
rafael
commented
We could convert the We could convert the `MemberForm` into a LiveComponent. This way, it gets its own `mount` function which we can move some of this logic into, making the user form page a bit smaller and easier to read.
|
||||
initial_properties =
|
||||
Enum.map(property_types, fn pt ->
|
||||
%{
|
||||
"property_type_id" => pt.id,
|
||||
"value" => %{
|
||||
"type" => pt.value_type,
|
||||
"value" => nil,
|
||||
"_union_type" => Atom.to_string(pt.value_type)
|
||||
}
|
||||
}
|
||||
end)
|
||||
member_form = AshPhoenix.Form.for_create(
|
||||
Mv.Membership.Member,
|
||||
:create_member,
|
||||
domain: Mv.Membership,
|
||||
as: "member",
|
||||
params: %{"properties" => initial_properties},
|
||||
forms: [auto?: true]
|
||||
) |> to_form()
|
||||
|
||||
# Initialize member_form for existing users with members
|
||||
member_form = if user && user.member do
|
||||
{:ok, member_with_properties} = Ash.load(user.member, properties: [:property_type])
|
||||
|
||||
existing_properties =
|
||||
member_with_properties.properties
|
||||
|> Enum.map(& &1.property_type_id)
|
||||
|
||||
is_missing_property = fn i ->
|
||||
not Enum.member?(existing_properties, Map.get(i, "property_type_id"))
|
||||
end
|
||||
|
||||
params = %{
|
||||
"properties" =>
|
||||
Enum.map(member_with_properties.properties, fn prop ->
|
||||
%{
|
||||
"property_type_id" => prop.property_type_id,
|
||||
"value" => %{
|
||||
"_union_type" => Atom.to_string(prop.value.type),
|
||||
"type" => prop.value.type,
|
||||
"value" => prop.value.value
|
||||
}
|
||||
}
|
||||
end)
|
||||
}
|
||||
|
||||
form =
|
||||
AshPhoenix.Form.for_update(
|
||||
member_with_properties,
|
||||
:update_member,
|
||||
domain: Mv.Membership,
|
||||
as: "member",
|
||||
params: params,
|
||||
forms: [auto?: true]
|
||||
)
|
||||
|
||||
missing_properties = Enum.filter(initial_properties, is_missing_property)
|
||||
|
||||
Enum.reduce(
|
||||
missing_properties,
|
||||
form,
|
||||
&AshPhoenix.Form.add_form(&2, [:properties], params: &1)
|
||||
) |> to_form()
|
||||
else
|
||||
member_form
|
||||
end
|
||||
|
||||
{:ok,
|
||||
socket
|
||||
|> assign(:return_to, return_to(params["return_to"]))
|
||||
|
|
@ -175,6 +285,9 @@ defmodule MvWeb.UserLive.Form do
|
|||
|> assign(:show_password_fields, false)
|
||||
|> assign(:member_assignment_mode, "create_new")
|
||||
|> assign(:available_members, available_member_options)
|
||||
|> assign(:property_types, property_types)
|
||||
|> assign(:initial_properties, initial_properties)
|
||||
|> assign(:member_form, member_form)
|
||||
|> assign_form()}
|
||||
end
|
||||
|
||||
|
|
@ -202,22 +315,46 @@ defmodule MvWeb.UserLive.Form do
|
|||
{:noreply, socket}
|
||||
end
|
||||
|
||||
def handle_event("validate", %{"user" => user_params, "member" => member_params}, socket) do
|
||||
member_form = AshPhoenix.Form.validate(socket.assigns.member_form.source, member_params) |> to_form()
|
||||
user_form = AshPhoenix.Form.validate(socket.assigns.form.source, user_params)
|
||||
|
carla
commented
I had to change this to: I had to change this to:
`user_form = AshPhoenix.Form.validate(socket.assigns.form.source, user_params) |> to_form()`
to make it work
|
||||
{:noreply, assign(socket, form: user_form, member_form: member_form)}
|
||||
end
|
||||
|
||||
def handle_event("validate", %{"user" => user_params}, socket) do
|
||||
{:noreply, assign(socket, form: AshPhoenix.Form.validate(socket.assigns.form, user_params))}
|
||||
end
|
||||
|
||||
def handle_event("save", %{"user" => user_params}, socket) do
|
||||
def handle_event("validate_member", %{"member" => member_params}, socket) do
|
||||
member_form = AshPhoenix.Form.validate(socket.assigns.member_form.source, member_params) |> to_form()
|
||||
{:noreply, assign(socket, member_form: member_form)}
|
||||
end
|
||||
|
||||
def handle_event("save_member", %{"member" => member_params}, socket) do
|
||||
case AshPhoenix.Form.submit(socket.assigns.member_form.source, params: member_params) do
|
||||
{:ok, member} ->
|
||||
# Reload the user with updated member
|
||||
user = socket.assigns.user |> Ash.load!(:member)
|
||||
socket =
|
||||
socket
|
||||
|> assign(user: user)
|
||||
|> put_flash(:info, "Member updated successfully")
|
||||
{:noreply, socket}
|
||||
{:error, member_form} ->
|
||||
{:noreply, assign(socket, member_form: to_form(member_form))}
|
||||
end
|
||||
end
|
||||
|
||||
def handle_event("save", params, socket) do
|
||||
user_params = params["user"] || %{}
|
||||
case AshPhoenix.Form.submit(socket.assigns.form, params: user_params) do
|
||||
{:ok, user} ->
|
||||
notify_parent({:saved, user})
|
||||
|
||||
socket =
|
||||
socket
|
||||
|> put_flash(:info, "User #{socket.assigns.form.source.type}d successfully")
|
||||
|> push_navigate(to: return_path(socket.assigns.return_to, user))
|
||||
|
||||
{:noreply, socket}
|
||||
|
||||
{:error, form} ->
|
||||
{:noreply, assign(socket, form: form)}
|
||||
end
|
||||
|
|
@ -225,36 +362,72 @@ defmodule MvWeb.UserLive.Form do
|
|||
|
||||
defp notify_parent(msg), do: send(self(), {__MODULE__, msg})
|
||||
|
||||
defp assign_form(%{assigns: %{user: user, show_password_fields: show_password_fields}} = socket) do
|
||||
defp assign_form(%{assigns: %{user: user, show_password_fields: show_password_fields, member_assignment_mode: member_assignment_mode, property_types: property_types}} = socket) do
|
||||
form =
|
||||
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,
|
||||
as: "user",
|
||||
actor: socket.assigns.current_user,
|
||||
domain: Mv.Accounts
|
||||
)
|
||||
if member_assignment_mode == "create_new" do
|
||||
initial_properties =
|
||||
Enum.map(property_types, fn pt ->
|
||||
%{
|
||||
"property_type_id" => pt.id,
|
||||
"value" => %{
|
||||
"type" => pt.value_type,
|
||||
"value" => nil,
|
||||
"_union_type" => Atom.to_string(pt.value_type)
|
||||
}
|
||||
}
|
||||
end)
|
||||
params = %{"member" => %{"properties" => initial_properties}}
|
||||
AshPhoenix.Form.for_update(user, action,
|
||||
as: "user",
|
||||
actor: socket.assigns.current_user,
|
||||
domain: Mv.Accounts,
|
||||
params: params,
|
||||
forms: [auto?: true]
|
||||
)
|
||||
else
|
||||
AshPhoenix.Form.for_update(user, action,
|
||||
as: "user",
|
||||
actor: socket.assigns.current_user,
|
||||
domain: Mv.Accounts
|
||||
)
|
||||
end
|
||||
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,
|
||||
accept: accept
|
||||
)
|
||||
if member_assignment_mode == "create_new" do
|
||||
initial_properties =
|
||||
Enum.map(property_types, fn pt ->
|
||||
%{
|
||||
"property_type_id" => pt.id,
|
||||
"value" => %{
|
||||
"type" => pt.value_type,
|
||||
"value" => nil,
|
||||
"_union_type" => Atom.to_string(pt.value_type)
|
||||
}
|
||||
}
|
||||
end)
|
||||
params = %{"member" => %{"properties" => initial_properties}}
|
||||
AshPhoenix.Form.for_create(Mv.Accounts.User, action,
|
||||
as: "user",
|
||||
actor: socket.assigns.current_user,
|
||||
domain: Mv.Accounts,
|
||||
accept: [:email, :password, :password_confirmation],
|
||||
params: params,
|
||||
forms: [auto?: true]
|
||||
)
|
||||
else
|
||||
AshPhoenix.Form.for_create(Mv.Accounts.User, action,
|
||||
as: "user",
|
||||
actor: socket.assigns.current_user,
|
||||
domain: Mv.Accounts,
|
||||
accept: if(member_assignment_mode == "assign_existing", do: [:email, :member_id], else: [:email])
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
assign(socket, form: to_form(form))
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue
Does this mean that users will always have an associated member? I don't really remember what our plan was in this regard.
Also, I'm not sure how this interacts with the new
MemberCreationNotifierthat also creates members for users. Do we need both ways to create new members?