diff --git a/lib/towerops_web/live/admin/org_live/index.ex b/lib/towerops_web/live/admin/org_live/index.ex index f5dcfc8f..cf1b47e0 100644 --- a/lib/towerops_web/live/admin/org_live/index.ex +++ b/lib/towerops_web/live/admin/org_live/index.ex @@ -4,12 +4,18 @@ defmodule ToweropsWeb.Admin.OrgLive.Index do """ use ToweropsWeb, :live_view + import Ecto.Query + alias Towerops.Admin + alias Towerops.Billing alias Towerops.Organizations.Organization @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 = count_active_subscriptions() ip = case get_connect_info(socket, :peer_data) do @@ -24,17 +30,41 @@ defmodule ToweropsWeb.Admin.OrgLive.Index do |> assign(:organizations, orgs) |> assign(:client_ip, ip) |> assign(:editing_org, nil) - |> assign(:override_form, 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(:override_form, nil) + |> assign(:editing_global, false)} org_id -> org = Enum.find(socket.assigns.organizations, &(&1.id == org_id)) @@ -45,12 +75,14 @@ defmodule ToweropsWeb.Admin.OrgLive.Index do {:noreply, socket |> assign(:editing_org, org) - |> assign(:override_form, to_form(changeset))} + |> assign(:override_form, to_form(changeset)) + |> assign(:editing_global, false)} else {:noreply, socket |> assign(:editing_org, nil) - |> assign(:override_form, nil)} + |> assign(:override_form, nil) + |> assign(:editing_global, false)} end end end @@ -128,4 +160,107 @@ defmodule ToweropsWeb.Admin.OrgLive.Index do {: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 + errors = [] + + errors = + case Map.get(params, "default_free_devices") do + nil -> + errors + + value -> + 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 + + errors = + case Map.get(params, "default_price_per_device") do + nil -> + errors + + value -> + case Decimal.parse(value) do + {decimal, _} -> + if Decimal.compare(decimal, "0.01") in [:gt, :eq] and + Decimal.compare(decimal, "999.99") in [:lt, :eq] do + errors + else + [{:default_price_per_device, {"must be between 0.01 and 999.99", []}} | errors] + end + + :error -> + [{:default_price_per_device, {"must be a valid decimal", []}} | errors] + end + end + + errors + end + + defp count_active_subscriptions do + alias Towerops.Repo + + Repo.one( + from o in Organization, + where: o.subscription_status == "active" and not is_nil(o.stripe_subscription_id), + select: count(o.id) + ) + end end diff --git a/lib/towerops_web/live/admin/org_live/index.html.heex b/lib/towerops_web/live/admin/org_live/index.html.heex index 848e952e..9ff86c4f 100644 --- a/lib/towerops_web/live/admin/org_live/index.html.heex +++ b/lib/towerops_web/live/admin/org_live/index.html.heex @@ -5,6 +5,54 @@
{t_admin("Manage organizations")}
+ ++ Default pricing for all organizations (can be overridden per-org) +
++ {elem(error, 0)} +
+ <% end %> ++ {elem(error, 0)} +
+ <% end %> ++ This will update {@active_sub_count} active subscriptions to the new price. + Changes take effect at the next billing cycle. +
+