Use current_actor/1 helper in all LiveViews
Replace inconsistent actor access patterns with current_actor/1 helper and ensure actor is passed to all Ash operations for proper authorization.
This commit is contained in:
parent
74fe60f768
commit
cd7e6b0843
9 changed files with 268 additions and 57 deletions
|
|
@ -32,6 +32,9 @@ defmodule MvWeb.CustomFieldValueLive.Form do
|
||||||
"""
|
"""
|
||||||
use MvWeb, :live_view
|
use MvWeb, :live_view
|
||||||
|
|
||||||
|
on_mount {MvWeb.LiveHelpers, :ensure_user_role_loaded}
|
||||||
|
import MvWeb.LiveHelpers, only: [current_actor: 1]
|
||||||
|
|
||||||
@impl true
|
@impl true
|
||||||
def render(assigns) do
|
def render(assigns) do
|
||||||
~H"""
|
~H"""
|
||||||
|
|
@ -172,8 +175,9 @@ defmodule MvWeb.CustomFieldValueLive.Form do
|
||||||
page_title = action <> " " <> "Custom field value"
|
page_title = action <> " " <> "Custom field value"
|
||||||
|
|
||||||
# Load all CustomFields and Members for the selection fields
|
# Load all CustomFields and Members for the selection fields
|
||||||
custom_fields = Ash.read!(Mv.Membership.CustomField)
|
actor = current_actor(socket)
|
||||||
members = Ash.read!(Mv.Membership.Member)
|
custom_fields = Ash.read!(Mv.Membership.CustomField, actor: actor)
|
||||||
|
members = Ash.read!(Mv.Membership.Member, actor: actor)
|
||||||
|
|
||||||
{:ok,
|
{:ok,
|
||||||
socket
|
socket
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,8 @@ defmodule MvWeb.MemberLive.Form do
|
||||||
|
|
||||||
on_mount {MvWeb.LiveHelpers, :ensure_user_role_loaded}
|
on_mount {MvWeb.LiveHelpers, :ensure_user_role_loaded}
|
||||||
|
|
||||||
|
import MvWeb.LiveHelpers, only: [current_actor: 1]
|
||||||
|
|
||||||
alias Mv.MembershipFees
|
alias Mv.MembershipFees
|
||||||
alias Mv.MembershipFees.MembershipFeeType
|
alias Mv.MembershipFees.MembershipFeeType
|
||||||
alias MvWeb.Helpers.MembershipFeeHelpers
|
alias MvWeb.Helpers.MembershipFeeHelpers
|
||||||
|
|
@ -174,7 +176,7 @@ defmodule MvWeb.MemberLive.Form do
|
||||||
<select
|
<select
|
||||||
class="select select-bordered w-full"
|
class="select select-bordered w-full"
|
||||||
name={@form[:membership_fee_type_id].name}
|
name={@form[:membership_fee_type_id].name}
|
||||||
phx-change="validate_membership_fee_type"
|
phx-change="validate"
|
||||||
value={@form[:membership_fee_type_id].value || ""}
|
value={@form[:membership_fee_type_id].value || ""}
|
||||||
>
|
>
|
||||||
<option value="">{gettext("None")}</option>
|
<option value="">{gettext("None")}</option>
|
||||||
|
|
@ -225,7 +227,7 @@ defmodule MvWeb.MemberLive.Form do
|
||||||
@impl true
|
@impl true
|
||||||
def mount(params, _session, socket) do
|
def mount(params, _session, socket) do
|
||||||
# current_user should be set by on_mount hooks (LiveUserAuth + LiveHelpers)
|
# current_user should be set by on_mount hooks (LiveUserAuth + LiveHelpers)
|
||||||
actor = socket.assigns[:current_user] || socket.assigns.current_user
|
actor = current_actor(socket)
|
||||||
{:ok, custom_fields} = Mv.Membership.list_custom_fields()
|
{:ok, custom_fields} = Mv.Membership.list_custom_fields()
|
||||||
|
|
||||||
initial_custom_field_values =
|
initial_custom_field_values =
|
||||||
|
|
@ -269,28 +271,29 @@ defmodule MvWeb.MemberLive.Form do
|
||||||
|
|
||||||
@impl true
|
@impl true
|
||||||
def handle_event("validate", %{"member" => member_params}, socket) do
|
def handle_event("validate", %{"member" => member_params}, socket) do
|
||||||
validated_form = AshPhoenix.Form.validate(socket.assigns.form, member_params)
|
# Merge with existing form values to preserve unchanged fields (especially custom_field_values)
|
||||||
|
# Extract values directly from form fields to get current state
|
||||||
|
existing_values = get_existing_form_values(socket.assigns.form)
|
||||||
|
|
||||||
|
# Merge existing values with new params (new params take precedence)
|
||||||
|
merged_params = Map.merge(existing_values, member_params)
|
||||||
|
|
||||||
|
validated_form = AshPhoenix.Form.validate(socket.assigns.form, merged_params)
|
||||||
|
|
||||||
# Check for interval mismatch if membership_fee_type_id changed
|
# Check for interval mismatch if membership_fee_type_id changed
|
||||||
socket = check_interval_change(socket, member_params)
|
socket = check_interval_change(socket, merged_params)
|
||||||
|
|
||||||
{:noreply, assign(socket, form: validated_form)}
|
{:noreply, assign(socket, form: validated_form)}
|
||||||
end
|
end
|
||||||
|
|
||||||
def handle_event(
|
|
||||||
"validate_membership_fee_type",
|
|
||||||
%{"member" => %{"membership_fee_type_id" => fee_type_id}},
|
|
||||||
socket
|
|
||||||
) do
|
|
||||||
# Same validation as above, but triggered by select change
|
|
||||||
handle_event("validate", %{"member" => %{"membership_fee_type_id" => fee_type_id}}, socket)
|
|
||||||
end
|
|
||||||
|
|
||||||
def handle_event("save", %{"member" => member_params}, socket) do
|
def handle_event("save", %{"member" => member_params}, socket) do
|
||||||
try do
|
try do
|
||||||
actor = socket.assigns[:current_user] || socket.assigns.current_user
|
actor = current_actor(socket)
|
||||||
|
|
||||||
case AshPhoenix.Form.submit(socket.assigns.form, params: member_params, actor: actor) do
|
case AshPhoenix.Form.submit(socket.assigns.form,
|
||||||
|
params: member_params,
|
||||||
|
action_opts: [actor: actor]
|
||||||
|
) do
|
||||||
{:ok, member} ->
|
{:ok, member} ->
|
||||||
handle_save_success(socket, member)
|
handle_save_success(socket, member)
|
||||||
|
|
||||||
|
|
@ -483,4 +486,167 @@ defmodule MvWeb.MemberLive.Form do
|
||||||
defp custom_field_input_type(:date), do: "date"
|
defp custom_field_input_type(:date), do: "date"
|
||||||
defp custom_field_input_type(:email), do: "email"
|
defp custom_field_input_type(:email), do: "email"
|
||||||
defp custom_field_input_type(_), do: "text"
|
defp custom_field_input_type(_), do: "text"
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------
|
||||||
|
# Helper Functions for Form Value Preservation
|
||||||
|
# -----------------------------------------------------------------
|
||||||
|
|
||||||
|
# Helper to extract existing form values to preserve them when only one field changes
|
||||||
|
# This ensures custom_field_values and other fields are preserved when only the dropdown changes
|
||||||
|
defp get_existing_form_values(form) do
|
||||||
|
%{}
|
||||||
|
|> extract_form_value(form, :first_name, &to_string/1)
|
||||||
|
|> extract_form_value(form, :last_name, &to_string/1)
|
||||||
|
|> extract_form_value(form, :email, &to_string/1)
|
||||||
|
|> extract_form_value(form, :street, &to_string/1)
|
||||||
|
|> extract_form_value(form, :house_number, &to_string/1)
|
||||||
|
|> extract_form_value(form, :postal_code, &to_string/1)
|
||||||
|
|> extract_form_value(form, :city, &to_string/1)
|
||||||
|
|> extract_form_value(form, :join_date, &format_date_value/1)
|
||||||
|
|> extract_form_value(form, :exit_date, &format_date_value/1)
|
||||||
|
|> extract_form_value(form, :notes, &to_string/1)
|
||||||
|
|> extract_form_value(form, :membership_fee_type_id, &to_string/1)
|
||||||
|
|> extract_form_value(form, :membership_fee_start_date, &format_date_value/1)
|
||||||
|
|> extract_custom_field_values(form)
|
||||||
|
end
|
||||||
|
|
||||||
|
# Helper to extract a single form field value
|
||||||
|
defp extract_form_value(acc, form, field, formatter) do
|
||||||
|
if form[field] && form[field].value do
|
||||||
|
Map.put(acc, to_string(field), formatter.(form[field].value))
|
||||||
|
else
|
||||||
|
acc
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Extracts custom field values from the form structure
|
||||||
|
# The form is a Phoenix.HTML.Form with source being AshPhoenix.Form
|
||||||
|
# Custom field values are in form.source.params["custom_field_values"] as a map
|
||||||
|
defp extract_custom_field_values(acc, form) do
|
||||||
|
cfv_params = get_custom_field_values_params(form)
|
||||||
|
|
||||||
|
if map_size(cfv_params) > 0 do
|
||||||
|
custom_field_values = convert_cfv_params_to_list(cfv_params)
|
||||||
|
Map.put(acc, "custom_field_values", custom_field_values)
|
||||||
|
else
|
||||||
|
acc
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Gets custom_field_values from form params
|
||||||
|
defp get_custom_field_values_params(form) do
|
||||||
|
ash_form = form.source
|
||||||
|
|
||||||
|
if ash_form && Map.has_key?(ash_form, :params) && ash_form.params["custom_field_values"] do
|
||||||
|
ash_form.params["custom_field_values"]
|
||||||
|
else
|
||||||
|
%{}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Converts custom field values map to sorted list
|
||||||
|
defp convert_cfv_params_to_list(cfv_params) do
|
||||||
|
cfv_params
|
||||||
|
|> Map.to_list()
|
||||||
|
|> Enum.sort_by(&parse_numeric_key/1)
|
||||||
|
|> Enum.map(&build_custom_field_value/1)
|
||||||
|
end
|
||||||
|
|
||||||
|
# Parses numeric key for sorting
|
||||||
|
defp parse_numeric_key({key, _}) do
|
||||||
|
case Integer.parse(key) do
|
||||||
|
{num, _} -> num
|
||||||
|
:error -> 999_999
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Builds a custom field value map from params
|
||||||
|
defp build_custom_field_value({_key, cfv_map}) do
|
||||||
|
%{
|
||||||
|
"custom_field_id" => Map.get(cfv_map, "custom_field_id", ""),
|
||||||
|
"value" => extract_custom_field_value_from_map(Map.get(cfv_map, "value", %{}))
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
# Extracts the value map structure from a custom field value
|
||||||
|
# Handles both map format and Ash.Union struct format
|
||||||
|
defp extract_custom_field_value_from_map(%Ash.Union{} = union) do
|
||||||
|
union_type = Atom.to_string(union.type)
|
||||||
|
|
||||||
|
%{
|
||||||
|
"_union_type" => union_type,
|
||||||
|
"type" => union_type,
|
||||||
|
"value" => format_custom_field_value(union.value)
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
defp extract_custom_field_value_from_map(value_map) when is_map(value_map) do
|
||||||
|
union_type = extract_union_type_from_map(value_map)
|
||||||
|
value = Map.get(value_map, "value") || Map.get(value_map, :value)
|
||||||
|
|
||||||
|
%{
|
||||||
|
"_union_type" => union_type,
|
||||||
|
"type" => union_type,
|
||||||
|
"value" => format_custom_field_value(value)
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
defp extract_custom_field_value_from_map(_),
|
||||||
|
do: %{"_union_type" => "", "type" => "", "value" => ""}
|
||||||
|
|
||||||
|
# Extracts union type from map, checking various possible locations
|
||||||
|
defp extract_union_type_from_map(value_map) do
|
||||||
|
cond do
|
||||||
|
has_non_empty_string(value_map, "_union_type") ->
|
||||||
|
Map.get(value_map, "_union_type")
|
||||||
|
|
||||||
|
has_non_empty_atom(value_map, :_union_type) ->
|
||||||
|
to_string(Map.get(value_map, :_union_type))
|
||||||
|
|
||||||
|
has_atom_type(value_map) ->
|
||||||
|
Atom.to_string(Map.get(value_map, :type))
|
||||||
|
|
||||||
|
has_string_type(value_map) ->
|
||||||
|
Map.get(value_map, "type")
|
||||||
|
|
||||||
|
true ->
|
||||||
|
""
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Helper to check if map has non-empty string value
|
||||||
|
defp has_non_empty_string(map, key) do
|
||||||
|
value = Map.get(map, key)
|
||||||
|
value && value != ""
|
||||||
|
end
|
||||||
|
|
||||||
|
# Helper to check if map has non-empty atom value
|
||||||
|
defp has_non_empty_atom(map, key) do
|
||||||
|
value = Map.get(map, key)
|
||||||
|
value && value != ""
|
||||||
|
end
|
||||||
|
|
||||||
|
# Helper to check if map has atom type
|
||||||
|
defp has_atom_type(map) do
|
||||||
|
value = Map.get(map, :type)
|
||||||
|
value && is_atom(value)
|
||||||
|
end
|
||||||
|
|
||||||
|
# Helper to check if map has string type
|
||||||
|
defp has_string_type(map) do
|
||||||
|
value = Map.get(map, "type")
|
||||||
|
value && is_binary(value)
|
||||||
|
end
|
||||||
|
|
||||||
|
# Formats custom field value based on its type
|
||||||
|
defp format_custom_field_value(%Date{} = date), do: Date.to_iso8601(date)
|
||||||
|
defp format_custom_field_value(%Decimal{} = decimal), do: Decimal.to_string(decimal, :normal)
|
||||||
|
defp format_custom_field_value(value) when is_boolean(value), do: to_string(value)
|
||||||
|
defp format_custom_field_value(value) when is_binary(value), do: value
|
||||||
|
defp format_custom_field_value(value), do: to_string(value)
|
||||||
|
|
||||||
|
# Formats date value (Date or string) to string
|
||||||
|
defp format_date_value(%Date{} = date), do: Date.to_iso8601(date)
|
||||||
|
defp format_date_value(value) when is_binary(value), do: value
|
||||||
|
defp format_date_value(_), do: ""
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,7 @@ defmodule MvWeb.MemberLive.Show do
|
||||||
"""
|
"""
|
||||||
use MvWeb, :live_view
|
use MvWeb, :live_view
|
||||||
import Ash.Query
|
import Ash.Query
|
||||||
|
import MvWeb.LiveHelpers, only: [current_actor: 1]
|
||||||
|
|
||||||
on_mount {MvWeb.LiveHelpers, :ensure_user_role_loaded}
|
on_mount {MvWeb.LiveHelpers, :ensure_user_role_loaded}
|
||||||
|
|
||||||
|
|
@ -236,7 +237,7 @@ defmodule MvWeb.MemberLive.Show do
|
||||||
|
|
||||||
@impl true
|
@impl true
|
||||||
def handle_params(%{"id" => id}, _, socket) do
|
def handle_params(%{"id" => id}, _, socket) do
|
||||||
actor = socket.assigns[:current_user]
|
actor = current_actor(socket)
|
||||||
|
|
||||||
# Load custom fields once using assign_new to avoid repeated queries
|
# Load custom fields once using assign_new to avoid repeated queries
|
||||||
socket =
|
socket =
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@ defmodule MvWeb.MemberLive.Show.MembershipFeesComponent do
|
||||||
use MvWeb, :live_component
|
use MvWeb, :live_component
|
||||||
|
|
||||||
require Ash.Query
|
require Ash.Query
|
||||||
|
import MvWeb.LiveHelpers, only: [current_actor: 1]
|
||||||
|
|
||||||
alias Mv.Membership
|
alias Mv.Membership
|
||||||
alias Mv.MembershipFees
|
alias Mv.MembershipFees
|
||||||
|
|
@ -426,7 +427,7 @@ defmodule MvWeb.MemberLive.Show.MembershipFeesComponent do
|
||||||
@impl true
|
@impl true
|
||||||
def handle_event("change_membership_fee_type", %{"value" => ""}, socket) do
|
def handle_event("change_membership_fee_type", %{"value" => ""}, socket) do
|
||||||
# Remove membership fee type
|
# Remove membership fee type
|
||||||
actor = socket.assigns.current_user
|
actor = current_actor(socket)
|
||||||
|
|
||||||
case update_member_fee_type(socket.assigns.member, nil, actor) do
|
case update_member_fee_type(socket.assigns.member, nil, actor) do
|
||||||
{:ok, updated_member} ->
|
{:ok, updated_member} ->
|
||||||
|
|
@ -438,7 +439,7 @@ defmodule MvWeb.MemberLive.Show.MembershipFeesComponent do
|
||||||
|> assign(:cycles, [])
|
|> assign(:cycles, [])
|
||||||
|> assign(
|
|> assign(
|
||||||
:available_fee_types,
|
:available_fee_types,
|
||||||
get_available_fee_types(updated_member, socket.assigns.current_user)
|
get_available_fee_types(updated_member, current_actor(socket))
|
||||||
)
|
)
|
||||||
|> assign(:interval_warning, nil)
|
|> assign(:interval_warning, nil)
|
||||||
|> put_flash(:info, gettext("Membership fee type removed"))}
|
|> put_flash(:info, gettext("Membership fee type removed"))}
|
||||||
|
|
@ -450,7 +451,7 @@ defmodule MvWeb.MemberLive.Show.MembershipFeesComponent do
|
||||||
|
|
||||||
def handle_event("change_membership_fee_type", %{"value" => fee_type_id}, socket) do
|
def handle_event("change_membership_fee_type", %{"value" => fee_type_id}, socket) do
|
||||||
member = socket.assigns.member
|
member = socket.assigns.member
|
||||||
actor = socket.assigns.current_user
|
actor = current_actor(socket)
|
||||||
new_fee_type = Ash.get!(MembershipFeeType, fee_type_id, domain: MembershipFees, actor: actor)
|
new_fee_type = Ash.get!(MembershipFeeType, fee_type_id, domain: MembershipFees, actor: actor)
|
||||||
|
|
||||||
# Check if interval matches
|
# Check if interval matches
|
||||||
|
|
@ -469,12 +470,12 @@ defmodule MvWeb.MemberLive.Show.MembershipFeesComponent do
|
||||||
if interval_warning do
|
if interval_warning do
|
||||||
{:noreply, assign(socket, :interval_warning, interval_warning)}
|
{:noreply, assign(socket, :interval_warning, interval_warning)}
|
||||||
else
|
else
|
||||||
actor = socket.assigns.current_user
|
actor = current_actor(socket)
|
||||||
|
|
||||||
case update_member_fee_type(member, fee_type_id, actor) do
|
case update_member_fee_type(member, fee_type_id, actor) do
|
||||||
{:ok, updated_member} ->
|
{:ok, updated_member} ->
|
||||||
# Reload member with cycles
|
# Reload member with cycles
|
||||||
actor = socket.assigns.current_user
|
actor = current_actor(socket)
|
||||||
|
|
||||||
updated_member =
|
updated_member =
|
||||||
updated_member
|
updated_member
|
||||||
|
|
@ -501,7 +502,7 @@ defmodule MvWeb.MemberLive.Show.MembershipFeesComponent do
|
||||||
|> assign(:cycles, cycles)
|
|> assign(:cycles, cycles)
|
||||||
|> assign(
|
|> assign(
|
||||||
:available_fee_types,
|
:available_fee_types,
|
||||||
get_available_fee_types(updated_member, socket.assigns.current_user)
|
get_available_fee_types(updated_member, current_actor(socket))
|
||||||
)
|
)
|
||||||
|> assign(:interval_warning, nil)
|
|> assign(:interval_warning, nil)
|
||||||
|> put_flash(:info, gettext("Membership fee type updated. Cycles regenerated."))}
|
|> put_flash(:info, gettext("Membership fee type updated. Cycles regenerated."))}
|
||||||
|
|
@ -523,7 +524,7 @@ defmodule MvWeb.MemberLive.Show.MembershipFeesComponent do
|
||||||
:suspended -> :mark_as_suspended
|
:suspended -> :mark_as_suspended
|
||||||
end
|
end
|
||||||
|
|
||||||
actor = socket.assigns.current_user
|
actor = current_actor(socket)
|
||||||
|
|
||||||
case Ash.update(cycle, action: action, domain: MembershipFees, actor: actor) do
|
case Ash.update(cycle, action: action, domain: MembershipFees, actor: actor) do
|
||||||
{:ok, updated_cycle} ->
|
{:ok, updated_cycle} ->
|
||||||
|
|
@ -555,11 +556,12 @@ defmodule MvWeb.MemberLive.Show.MembershipFeesComponent do
|
||||||
def handle_event("regenerate_cycles", _params, socket) do
|
def handle_event("regenerate_cycles", _params, socket) do
|
||||||
socket = assign(socket, :regenerating, true)
|
socket = assign(socket, :regenerating, true)
|
||||||
member = socket.assigns.member
|
member = socket.assigns.member
|
||||||
|
actor = current_actor(socket)
|
||||||
|
|
||||||
case CycleGenerator.generate_cycles_for_member(member.id) do
|
case CycleGenerator.generate_cycles_for_member(member.id, actor: actor) do
|
||||||
{:ok, _new_cycles, _notifications} ->
|
{:ok, _new_cycles, _notifications} ->
|
||||||
# Reload member with cycles
|
# Reload member with cycles
|
||||||
actor = socket.assigns.current_user
|
actor = current_actor(socket)
|
||||||
|
|
||||||
updated_member =
|
updated_member =
|
||||||
member
|
member
|
||||||
|
|
@ -599,7 +601,7 @@ defmodule MvWeb.MemberLive.Show.MembershipFeesComponent do
|
||||||
cycle = find_cycle(socket.assigns.cycles, cycle_id)
|
cycle = find_cycle(socket.assigns.cycles, cycle_id)
|
||||||
|
|
||||||
# Load cycle with membership_fee_type for display
|
# Load cycle with membership_fee_type for display
|
||||||
actor = socket.assigns.current_user
|
actor = current_actor(socket)
|
||||||
cycle = Ash.load!(cycle, :membership_fee_type, actor: actor)
|
cycle = Ash.load!(cycle, :membership_fee_type, actor: actor)
|
||||||
|
|
||||||
{:noreply, assign(socket, :editing_cycle, cycle)}
|
{:noreply, assign(socket, :editing_cycle, cycle)}
|
||||||
|
|
@ -617,7 +619,7 @@ defmodule MvWeb.MemberLive.Show.MembershipFeesComponent do
|
||||||
|
|
||||||
case Decimal.parse(normalized_amount_str) do
|
case Decimal.parse(normalized_amount_str) do
|
||||||
{amount, _} when is_struct(amount, Decimal) ->
|
{amount, _} when is_struct(amount, Decimal) ->
|
||||||
actor = socket.assigns.current_user
|
actor = current_actor(socket)
|
||||||
|
|
||||||
case cycle
|
case cycle
|
||||||
|> Ash.Changeset.for_update(:update, %{amount: amount})
|
|> Ash.Changeset.for_update(:update, %{amount: amount})
|
||||||
|
|
@ -646,7 +648,7 @@ defmodule MvWeb.MemberLive.Show.MembershipFeesComponent do
|
||||||
cycle = find_cycle(socket.assigns.cycles, cycle_id)
|
cycle = find_cycle(socket.assigns.cycles, cycle_id)
|
||||||
|
|
||||||
# Load cycle with membership_fee_type for display
|
# Load cycle with membership_fee_type for display
|
||||||
actor = socket.assigns.current_user
|
actor = current_actor(socket)
|
||||||
cycle = Ash.load!(cycle, :membership_fee_type, actor: actor)
|
cycle = Ash.load!(cycle, :membership_fee_type, actor: actor)
|
||||||
|
|
||||||
{:noreply, assign(socket, :deleting_cycle, cycle)}
|
{:noreply, assign(socket, :deleting_cycle, cycle)}
|
||||||
|
|
@ -658,7 +660,7 @@ defmodule MvWeb.MemberLive.Show.MembershipFeesComponent do
|
||||||
|
|
||||||
def handle_event("confirm_delete_cycle", %{"cycle_id" => cycle_id}, socket) do
|
def handle_event("confirm_delete_cycle", %{"cycle_id" => cycle_id}, socket) do
|
||||||
cycle = find_cycle(socket.assigns.cycles, cycle_id)
|
cycle = find_cycle(socket.assigns.cycles, cycle_id)
|
||||||
actor = socket.assigns.current_user
|
actor = current_actor(socket)
|
||||||
|
|
||||||
case Ash.destroy(cycle, domain: MembershipFees, actor: actor) do
|
case Ash.destroy(cycle, domain: MembershipFees, actor: actor) do
|
||||||
:ok ->
|
:ok ->
|
||||||
|
|
@ -731,7 +733,7 @@ defmodule MvWeb.MemberLive.Show.MembershipFeesComponent do
|
||||||
|
|
||||||
if deleted_count > 0 do
|
if deleted_count > 0 do
|
||||||
# Reload member to get updated cycles
|
# Reload member to get updated cycles
|
||||||
actor = socket.assigns.current_user
|
actor = current_actor(socket)
|
||||||
|
|
||||||
updated_member =
|
updated_member =
|
||||||
member
|
member
|
||||||
|
|
@ -823,7 +825,7 @@ defmodule MvWeb.MemberLive.Show.MembershipFeesComponent do
|
||||||
membership_fee_type_id: member.membership_fee_type_id
|
membership_fee_type_id: member.membership_fee_type_id
|
||||||
}
|
}
|
||||||
|
|
||||||
actor = socket.assigns.current_user
|
actor = current_actor(socket)
|
||||||
|
|
||||||
case Ash.create(MembershipFeeCycle, attrs, domain: MembershipFees, actor: actor) do
|
case Ash.create(MembershipFeeCycle, attrs, domain: MembershipFees, actor: actor) do
|
||||||
{:ok, _new_cycle} ->
|
{:ok, _new_cycle} ->
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,9 @@ defmodule MvWeb.MembershipFeeTypeLive.Form do
|
||||||
"""
|
"""
|
||||||
use MvWeb, :live_view
|
use MvWeb, :live_view
|
||||||
|
|
||||||
|
on_mount {MvWeb.LiveHelpers, :ensure_user_role_loaded}
|
||||||
|
import MvWeb.LiveHelpers, only: [current_actor: 1]
|
||||||
|
|
||||||
require Ash.Query
|
require Ash.Query
|
||||||
|
|
||||||
alias Mv.Membership.Member
|
alias Mv.Membership.Member
|
||||||
|
|
@ -380,7 +383,7 @@ defmodule MvWeb.MembershipFeeTypeLive.Form do
|
||||||
@spec return_path(String.t(), MembershipFeeType.t() | nil) :: String.t()
|
@spec return_path(String.t(), MembershipFeeType.t() | nil) :: String.t()
|
||||||
defp return_path("index", _membership_fee_type), do: ~p"/membership_fee_types"
|
defp return_path("index", _membership_fee_type), do: ~p"/membership_fee_types"
|
||||||
|
|
||||||
@spec get_affected_member_count(String.t()) :: non_neg_integer()
|
@spec get_affected_member_count(String.t(), Mv.Accounts.User.t() | nil) :: non_neg_integer()
|
||||||
# Checks if amount changed and updates socket assigns accordingly
|
# Checks if amount changed and updates socket assigns accordingly
|
||||||
defp check_amount_change(socket, params) do
|
defp check_amount_change(socket, params) do
|
||||||
if socket.assigns.membership_fee_type && Map.has_key?(params, "amount") do
|
if socket.assigns.membership_fee_type && Map.has_key?(params, "amount") do
|
||||||
|
|
@ -428,7 +431,7 @@ defmodule MvWeb.MembershipFeeTypeLive.Form do
|
||||||
socket.assigns.affected_member_count
|
socket.assigns.affected_member_count
|
||||||
else
|
else
|
||||||
# Warning being shown for first time, calculate count
|
# Warning being shown for first time, calculate count
|
||||||
get_affected_member_count(socket.assigns.membership_fee_type.id)
|
get_affected_member_count(socket.assigns.membership_fee_type.id, current_actor(socket))
|
||||||
end
|
end
|
||||||
|
|
||||||
socket
|
socket
|
||||||
|
|
@ -446,8 +449,10 @@ defmodule MvWeb.MembershipFeeTypeLive.Form do
|
||||||
|> assign(:pending_amount, nil)
|
|> assign(:pending_amount, nil)
|
||||||
end
|
end
|
||||||
|
|
||||||
defp get_affected_member_count(fee_type_id) do
|
defp get_affected_member_count(fee_type_id, actor) do
|
||||||
case Ash.count(Member |> Ash.Query.filter(membership_fee_type_id == ^fee_type_id)) do
|
case Ash.count(Member |> Ash.Query.filter(membership_fee_type_id == ^fee_type_id),
|
||||||
|
actor: actor
|
||||||
|
) do
|
||||||
{:ok, count} -> count
|
{:ok, count} -> count
|
||||||
_ -> 0
|
_ -> 0
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,9 @@ defmodule MvWeb.MembershipFeeTypeLive.Index do
|
||||||
"""
|
"""
|
||||||
use MvWeb, :live_view
|
use MvWeb, :live_view
|
||||||
|
|
||||||
|
on_mount {MvWeb.LiveHelpers, :ensure_user_role_loaded}
|
||||||
|
import MvWeb.LiveHelpers, only: [current_actor: 1]
|
||||||
|
|
||||||
require Ash.Query
|
require Ash.Query
|
||||||
|
|
||||||
alias Mv.Membership
|
alias Mv.Membership
|
||||||
|
|
@ -24,8 +27,9 @@ defmodule MvWeb.MembershipFeeTypeLive.Index do
|
||||||
|
|
||||||
@impl true
|
@impl true
|
||||||
def mount(_params, _session, socket) do
|
def mount(_params, _session, socket) do
|
||||||
fee_types = load_membership_fee_types()
|
actor = current_actor(socket)
|
||||||
member_counts = load_member_counts(fee_types)
|
fee_types = load_membership_fee_types(actor)
|
||||||
|
member_counts = load_member_counts(fee_types, actor)
|
||||||
|
|
||||||
{:ok,
|
{:ok,
|
||||||
socket
|
socket
|
||||||
|
|
@ -149,14 +153,14 @@ defmodule MvWeb.MembershipFeeTypeLive.Index do
|
||||||
|
|
||||||
# Helper functions
|
# Helper functions
|
||||||
|
|
||||||
defp load_membership_fee_types do
|
defp load_membership_fee_types(actor) do
|
||||||
MembershipFeeType
|
MembershipFeeType
|
||||||
|> Ash.Query.sort(name: :asc)
|
|> Ash.Query.sort(name: :asc)
|
||||||
|> Ash.read!(domain: MembershipFees)
|
|> Ash.read!(domain: MembershipFees, actor: actor)
|
||||||
end
|
end
|
||||||
|
|
||||||
# Loads all member counts for fee types in a single query to avoid N+1 queries
|
# Loads all member counts for fee types in a single query to avoid N+1 queries
|
||||||
defp load_member_counts(fee_types) do
|
defp load_member_counts(fee_types, actor) do
|
||||||
fee_type_ids = Enum.map(fee_types, & &1.id)
|
fee_type_ids = Enum.map(fee_types, & &1.id)
|
||||||
|
|
||||||
# Load all members with membership_fee_type_id in a single query
|
# Load all members with membership_fee_type_id in a single query
|
||||||
|
|
@ -164,7 +168,7 @@ defmodule MvWeb.MembershipFeeTypeLive.Index do
|
||||||
Member
|
Member
|
||||||
|> Ash.Query.filter(membership_fee_type_id in ^fee_type_ids)
|
|> Ash.Query.filter(membership_fee_type_id in ^fee_type_ids)
|
||||||
|> Ash.Query.select([:membership_fee_type_id])
|
|> Ash.Query.select([:membership_fee_type_id])
|
||||||
|> Ash.read!(domain: Membership)
|
|> Ash.read!(domain: Membership, actor: actor)
|
||||||
|
|
||||||
# Group by membership_fee_type_id and count
|
# Group by membership_fee_type_id and count
|
||||||
members
|
members
|
||||||
|
|
|
||||||
|
|
@ -33,6 +33,9 @@ defmodule MvWeb.UserLive.Form do
|
||||||
"""
|
"""
|
||||||
use MvWeb, :live_view
|
use MvWeb, :live_view
|
||||||
|
|
||||||
|
on_mount {MvWeb.LiveHelpers, :ensure_user_role_loaded}
|
||||||
|
import MvWeb.LiveHelpers, only: [current_actor: 1]
|
||||||
|
|
||||||
@impl true
|
@impl true
|
||||||
def render(assigns) do
|
def render(assigns) do
|
||||||
~H"""
|
~H"""
|
||||||
|
|
@ -258,10 +261,12 @@ defmodule MvWeb.UserLive.Form do
|
||||||
|
|
||||||
@impl true
|
@impl true
|
||||||
def mount(params, _session, socket) do
|
def mount(params, _session, socket) do
|
||||||
|
actor = current_actor(socket)
|
||||||
|
|
||||||
user =
|
user =
|
||||||
case params["id"] do
|
case params["id"] do
|
||||||
nil -> nil
|
nil -> nil
|
||||||
id -> Ash.get!(Mv.Accounts.User, id, domain: Mv.Accounts, load: [:member])
|
id -> Ash.get!(Mv.Accounts.User, id, domain: Mv.Accounts, load: [:member], actor: actor)
|
||||||
end
|
end
|
||||||
|
|
||||||
action = if is_nil(user), do: gettext("New"), else: gettext("Edit")
|
action = if is_nil(user), do: gettext("New"), else: gettext("Edit")
|
||||||
|
|
@ -307,7 +312,7 @@ defmodule MvWeb.UserLive.Form do
|
||||||
socket =
|
socket =
|
||||||
if Map.has_key?(user_params, "email") do
|
if Map.has_key?(user_params, "email") do
|
||||||
user_email = user_params["email"]
|
user_email = user_params["email"]
|
||||||
members = load_members_for_linking(user_email, socket.assigns.member_search_query)
|
members = load_members_for_linking(user_email, socket.assigns.member_search_query, socket)
|
||||||
|
|
||||||
assign(socket, form: validated_form, available_members: members)
|
assign(socket, form: validated_form, available_members: members)
|
||||||
else
|
else
|
||||||
|
|
@ -318,19 +323,27 @@ defmodule MvWeb.UserLive.Form do
|
||||||
end
|
end
|
||||||
|
|
||||||
def handle_event("save", %{"user" => user_params}, socket) do
|
def handle_event("save", %{"user" => user_params}, socket) do
|
||||||
|
actor = current_actor(socket)
|
||||||
# First save the user without member changes
|
# First save the user without member changes
|
||||||
case AshPhoenix.Form.submit(socket.assigns.form, params: user_params) do
|
case AshPhoenix.Form.submit(socket.assigns.form,
|
||||||
|
params: user_params,
|
||||||
|
action_opts: [actor: actor]
|
||||||
|
) do
|
||||||
{:ok, user} ->
|
{:ok, user} ->
|
||||||
# Then handle member linking/unlinking as a separate step
|
# Then handle member linking/unlinking as a separate step
|
||||||
|
actor = current_actor(socket)
|
||||||
|
|
||||||
result =
|
result =
|
||||||
cond do
|
cond do
|
||||||
# Selected member ID takes precedence (new link)
|
# Selected member ID takes precedence (new link)
|
||||||
socket.assigns.selected_member_id ->
|
socket.assigns.selected_member_id ->
|
||||||
Mv.Accounts.update_user(user, %{member: %{id: socket.assigns.selected_member_id}})
|
Mv.Accounts.update_user(user, %{member: %{id: socket.assigns.selected_member_id}},
|
||||||
|
actor: actor
|
||||||
|
)
|
||||||
|
|
||||||
# Unlink flag is set
|
# Unlink flag is set
|
||||||
socket.assigns[:unlink_member] ->
|
socket.assigns[:unlink_member] ->
|
||||||
Mv.Accounts.update_user(user, %{member: nil})
|
Mv.Accounts.update_user(user, %{member: nil}, actor: actor)
|
||||||
|
|
||||||
# No changes to member relationship
|
# No changes to member relationship
|
||||||
true ->
|
true ->
|
||||||
|
|
@ -497,18 +510,21 @@ defmodule MvWeb.UserLive.Form do
|
||||||
|
|
||||||
@spec assign_form(Phoenix.LiveView.Socket.t()) :: Phoenix.LiveView.Socket.t()
|
@spec assign_form(Phoenix.LiveView.Socket.t()) :: Phoenix.LiveView.Socket.t()
|
||||||
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}} = socket) do
|
||||||
|
actor = current_actor(socket)
|
||||||
|
|
||||||
form =
|
form =
|
||||||
if user do
|
if user do
|
||||||
# For existing users, use admin password action if password fields are shown
|
# For existing users, use admin password action if password fields are shown
|
||||||
action = if show_password_fields, do: :admin_set_password, else: :update_user
|
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, domain: Mv.Accounts, as: "user", actor: actor)
|
||||||
else
|
else
|
||||||
# For new users, use password registration if password fields are shown
|
# For new users, use password registration if password fields are shown
|
||||||
action = if show_password_fields, do: :register_with_password, else: :create_user
|
action = if show_password_fields, do: :register_with_password, else: :create_user
|
||||||
|
|
||||||
AshPhoenix.Form.for_create(Mv.Accounts.User, action,
|
AshPhoenix.Form.for_create(Mv.Accounts.User, action,
|
||||||
domain: Mv.Accounts,
|
domain: Mv.Accounts,
|
||||||
as: "user"
|
as: "user",
|
||||||
|
actor: actor
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -524,7 +540,7 @@ defmodule MvWeb.UserLive.Form do
|
||||||
user = socket.assigns.user
|
user = socket.assigns.user
|
||||||
user_email = if user, do: user.email, else: nil
|
user_email = if user, do: user.email, else: nil
|
||||||
|
|
||||||
members = load_members_for_linking(user_email, "")
|
members = load_members_for_linking(user_email, "", socket)
|
||||||
|
|
||||||
# Dropdown should ALWAYS be hidden initially
|
# Dropdown should ALWAYS be hidden initially
|
||||||
# It will only show when user focuses the input field (show_member_dropdown event)
|
# It will only show when user focuses the input field (show_member_dropdown event)
|
||||||
|
|
@ -539,12 +555,15 @@ defmodule MvWeb.UserLive.Form do
|
||||||
user = socket.assigns.user
|
user = socket.assigns.user
|
||||||
user_email = if user, do: user.email, else: nil
|
user_email = if user, do: user.email, else: nil
|
||||||
|
|
||||||
members = load_members_for_linking(user_email, query)
|
members = load_members_for_linking(user_email, query, socket)
|
||||||
assign(socket, available_members: members)
|
assign(socket, available_members: members)
|
||||||
end
|
end
|
||||||
|
|
||||||
@spec load_members_for_linking(String.t() | nil, String.t() | nil) :: [Mv.Membership.Member.t()]
|
@spec load_members_for_linking(String.t() | nil, String.t() | nil, Phoenix.LiveView.Socket.t()) ::
|
||||||
defp load_members_for_linking(user_email, search_query) do
|
[
|
||||||
|
Mv.Membership.Member.t()
|
||||||
|
]
|
||||||
|
defp load_members_for_linking(user_email, search_query, socket) do
|
||||||
user_email_str = if user_email, do: to_string(user_email), else: nil
|
user_email_str = if user_email, do: to_string(user_email), else: nil
|
||||||
search_query_str = if search_query && search_query != "", do: search_query, else: nil
|
search_query_str = if search_query && search_query != "", do: search_query, else: nil
|
||||||
|
|
||||||
|
|
@ -555,7 +574,9 @@ defmodule MvWeb.UserLive.Form do
|
||||||
search_query: search_query_str
|
search_query: search_query_str
|
||||||
})
|
})
|
||||||
|
|
||||||
case Ash.read(query, domain: Mv.Membership) do
|
actor = current_actor(socket)
|
||||||
|
|
||||||
|
case Ash.read(query, domain: Mv.Membership, actor: actor) do
|
||||||
{:ok, members} ->
|
{:ok, members} ->
|
||||||
# Apply email match filter if user_email is provided
|
# Apply email match filter if user_email is provided
|
||||||
if user_email_str do
|
if user_email_str do
|
||||||
|
|
|
||||||
|
|
@ -23,9 +23,13 @@ defmodule MvWeb.UserLive.Index do
|
||||||
use MvWeb, :live_view
|
use MvWeb, :live_view
|
||||||
import MvWeb.TableComponents
|
import MvWeb.TableComponents
|
||||||
|
|
||||||
|
on_mount {MvWeb.LiveHelpers, :ensure_user_role_loaded}
|
||||||
|
import MvWeb.LiveHelpers, only: [current_actor: 1]
|
||||||
|
|
||||||
@impl true
|
@impl true
|
||||||
def mount(_params, _session, socket) do
|
def mount(_params, _session, socket) do
|
||||||
users = Ash.read!(Mv.Accounts.User, domain: Mv.Accounts, load: [:member])
|
actor = current_actor(socket)
|
||||||
|
users = Ash.read!(Mv.Accounts.User, domain: Mv.Accounts, load: [:member], actor: actor)
|
||||||
sorted = Enum.sort_by(users, & &1.email)
|
sorted = Enum.sort_by(users, & &1.email)
|
||||||
|
|
||||||
{:ok,
|
{:ok,
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,9 @@ defmodule MvWeb.UserLive.Show do
|
||||||
"""
|
"""
|
||||||
use MvWeb, :live_view
|
use MvWeb, :live_view
|
||||||
|
|
||||||
|
on_mount {MvWeb.LiveHelpers, :ensure_user_role_loaded}
|
||||||
|
import MvWeb.LiveHelpers, only: [current_actor: 1]
|
||||||
|
|
||||||
@impl true
|
@impl true
|
||||||
def render(assigns) do
|
def render(assigns) do
|
||||||
~H"""
|
~H"""
|
||||||
|
|
@ -70,7 +73,8 @@ defmodule MvWeb.UserLive.Show do
|
||||||
|
|
||||||
@impl true
|
@impl true
|
||||||
def mount(%{"id" => id}, _session, socket) do
|
def mount(%{"id" => id}, _session, socket) do
|
||||||
user = Ash.get!(Mv.Accounts.User, id, domain: Mv.Accounts, load: [:member])
|
actor = current_actor(socket)
|
||||||
|
user = Ash.get!(Mv.Accounts.User, id, domain: Mv.Accounts, load: [:member], actor: actor)
|
||||||
|
|
||||||
{:ok,
|
{:ok,
|
||||||
socket
|
socket
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue