defmodule ToweropsWeb.Admin.OrgLive.Index do @moduledoc """ Admin interface for viewing and managing organizations. """ use ToweropsWeb, :live_view alias Towerops.Admin alias Towerops.Billing alias Towerops.Organizations.Organization on_mount {ToweropsWeb.UserAuth, :require_superuser} @impl true def mount(_params, _session, socket) do orgs = Admin.list_all_organizations() default_free_devices = Billing.default_free_devices() default_price = Billing.default_price_per_device() active_sub_count = Admin.count_active_subscriptions() ip = case get_connect_info(socket, :peer_data) do %{address: address} -> to_string(:inet_parse.ntoa(address)) _ -> "unknown" end {:ok, socket |> assign(:page_title, t("All Organizations")) |> assign(:timezone, socket.assigns.current_scope.timezone) |> assign(:organizations, orgs) |> assign(:client_ip, ip) |> assign(:editing_org, nil) |> assign(:override_form, nil) |> assign(:default_free_devices, default_free_devices) |> assign(:default_price, default_price) |> assign(:active_sub_count, active_sub_count) |> assign(:editing_global, false) |> assign(:global_form, nil) |> assign(:show_confirm, false) |> assign(:pending_pricing_params, nil)} end @impl true def handle_params(%{"edit_global" => "true"} = _params, _url, socket) do form = to_form(%{ "default_free_devices" => socket.assigns.default_free_devices, "default_price_per_device" => Decimal.to_string(socket.assigns.default_price) }) {:noreply, socket |> assign(:editing_global, true) |> assign(:global_form, form) |> assign(:show_confirm, false) |> assign(:editing_org, nil) |> assign(:override_form, nil)} end def handle_params(params, _url, socket) do case params["edit"] do nil -> {:noreply, socket |> assign(:editing_org, nil) |> assign(:override_form, nil) |> assign(:editing_global, false)} org_id -> org = Enum.find(socket.assigns.organizations, &(&1.id == org_id)) if org do changeset = Organization.billing_override_changeset(org, %{}) {:noreply, socket |> assign(:editing_org, org) |> assign(:override_form, to_form(changeset)) |> assign(:editing_global, false)} else {:noreply, socket |> assign(:editing_org, nil) |> assign(:override_form, nil) |> assign(:editing_global, false)} end end end @impl true def handle_event("edit_overrides", %{"id" => org_id}, socket) do {:noreply, push_patch(socket, to: ~p"/admin/organizations?edit=#{org_id}")} end def handle_event("validate_overrides", %{"organization" => params}, socket) do changeset = socket.assigns.editing_org |> Organization.billing_override_changeset(params) |> Map.put(:action, :validate) {:noreply, assign(socket, :override_form, to_form(changeset))} end def handle_event("save_overrides", %{"organization" => params}, socket) do superuser = socket.assigns.current_scope.superuser || socket.assigns.current_scope.user org = socket.assigns.editing_org case Admin.update_billing_overrides(org.id, params, superuser.id, socket.assigns.client_ip) do {:ok, _updated_org} -> orgs = Admin.list_all_organizations() {:noreply, socket |> put_flash(:info, t_admin("Billing overrides updated")) |> assign(:organizations, orgs) |> push_patch(to: ~p"/admin/organizations")} {:error, changeset} -> {:noreply, assign(socket, :override_form, to_form(changeset))} end end def handle_event("clear_overrides", _params, socket) do superuser = socket.assigns.current_scope.superuser || socket.assigns.current_scope.user org = socket.assigns.editing_org attrs = %{custom_free_device_limit: nil, custom_price_per_device: nil} case Admin.update_billing_overrides(org.id, attrs, superuser.id, socket.assigns.client_ip) do {:ok, _updated_org} -> orgs = Admin.list_all_organizations() {:noreply, socket |> put_flash(:info, t_admin("Billing overrides cleared")) |> assign(:organizations, orgs) |> push_patch(to: ~p"/admin/organizations")} {:error, _changeset} -> {:noreply, put_flash(socket, :error, t_admin("Failed to clear overrides"))} end end def handle_event("cancel_overrides", _params, socket) do {:noreply, push_patch(socket, to: ~p"/admin/organizations")} end def handle_event("delete_org", %{"id" => org_id}, socket) do superuser = socket.assigns.current_scope.superuser || socket.assigns.current_scope.user ip = socket.assigns.client_ip case Admin.delete_organization(org_id, superuser.id, ip) do {:ok, _} -> {:noreply, socket |> put_flash(:info, t_admin("Organization deleted successfully")) |> assign(:organizations, Admin.list_all_organizations())} {:error, _} -> {:noreply, put_flash(socket, :error, t_admin("Failed to delete organization"))} end end def handle_event("edit_global_defaults", _params, socket) do {:noreply, push_patch(socket, to: ~p"/admin/organizations?edit_global=true")} end def handle_event("validate_global_pricing", %{"global_pricing" => params}, socket) do errors = validate_global_pricing_params(params) form = to_form(params, errors: errors) {:noreply, assign(socket, :global_form, form)} end def handle_event("save_global_pricing", %{"global_pricing" => params}, socket) do case validate_global_pricing_params(params) do [] -> # Valid - show confirmation {:noreply, socket |> assign(:show_confirm, true) |> assign(:pending_pricing_params, params)} errors -> form = to_form(params, errors: errors) {:noreply, assign(socket, :global_form, form)} end end def handle_event("confirm_pricing_update", _params, socket) do params = socket.assigns.pending_pricing_params superuser = socket.assigns.current_scope.superuser || socket.assigns.current_scope.user case Admin.update_global_pricing(params, superuser.id, socket.assigns.client_ip) do {:ok, result} -> {:noreply, socket |> put_flash( :info, "Pricing updated successfully. #{result.succeeded}/#{result.total} subscriptions migrated." ) |> push_patch(to: ~p"/admin/organizations")} {:error, reason} -> {:noreply, put_flash(socket, :error, "Failed to update pricing: #{inspect(reason)}")} end end def handle_event("cancel_global_edit", _params, socket) do {:noreply, push_patch(socket, to: ~p"/admin/organizations")} end defp validate_global_pricing_params(params) do [] |> validate_free_devices(params) |> validate_price_per_device(params) end defp validate_free_devices(errors, params) do case Map.get(params, "default_free_devices") do nil -> errors value -> validate_free_devices_value(errors, value) end end defp validate_free_devices_value(errors, value) do case Integer.parse(value) do {int, _} when int > 0 and int < 10_000 -> errors _ -> [{:default_free_devices, {"must be greater than 0 and less than 10,000", []}} | errors] end end defp validate_price_per_device(errors, params) do case Map.get(params, "default_price_per_device") do nil -> errors value -> validate_price_per_device_value(errors, value) end end defp validate_price_per_device_value(errors, value) do case Decimal.parse(value) do {decimal, _} -> validate_price_range(errors, decimal) :error -> [{:default_price_per_device, {"must be a valid decimal", []}} | errors] end end defp validate_price_range(errors, decimal) do valid_range? = Decimal.compare(decimal, "0.01") in [:gt, :eq] and Decimal.compare(decimal, "999.99") in [:lt, :eq] if valid_range? do errors else [{:default_price_per_device, {"must be between 0.01 and 999.99", []}} | errors] end end end