End-to-end CRUD scaffold for per-site, per-antenna RF coverage prediction. Schema, context, antenna registry, Oban worker stub, and three LiveViews are wired up; the actual ITM + LIDAR + buildings compute pipeline lands in a follow-up. - Migration + Coverage schema with full validation (RF params, pixel- budget cap, antenna_slug existence). organization_id excluded from cast (programmatic-only). - Coverages context: org-scoped CRUD, validate_site_in_organization rejects cross-tenant site_id refs at insert/update, queue_compute with PubSub broadcast on per-coverage and per-org topics. - MSI Planet .ant parser + persistent_term registry loaded at boot from priv/antennas/. Two synthetic .ant fixtures (omni + 90deg sector) bundled. - CoverageWorker on new :coverage Oban queue (concurrency 2). Job args carry organization_id; worker refuses to run on org mismatch. Stub flips status to "failed" with "compute not yet implemented" so the UI flow is exercisable. - LiveViews: index (org-wide, live status updates), form (antenna picker grouped by manufacturer), show (status banners, params panel, map placeholder). Sidebar nav link added. - Routes /coverage, /coverage/new, /coverage/:id, /coverage/:id/edit under :require_authenticated_user_with_default_org. - 41 new tests covering schema validation, .ant parsing, context CRUD with cross-org guards, Oban job enqueue.
106 lines
3 KiB
Elixir
106 lines
3 KiB
Elixir
defmodule ToweropsWeb.CoverageLive.Form do
|
|
@moduledoc false
|
|
use ToweropsWeb, :live_view
|
|
|
|
alias Towerops.Coverages
|
|
alias Towerops.Coverages.Antenna
|
|
alias Towerops.Coverages.Coverage
|
|
alias Towerops.Sites
|
|
|
|
@impl true
|
|
def mount(_params, _session, socket) do
|
|
organization = socket.assigns.current_scope.organization
|
|
sites = Sites.list_organization_sites(organization.id)
|
|
antennas = Antenna.list()
|
|
|
|
{:ok,
|
|
socket
|
|
|> assign(:organization, organization)
|
|
|> assign(:sites, sites)
|
|
|> assign(:antennas, antennas)
|
|
|> assign(:antenna_options, antenna_options(antennas))}
|
|
end
|
|
|
|
@impl true
|
|
def handle_params(params, _url, socket) do
|
|
{:noreply, apply_action(socket, socket.assigns.live_action, params)}
|
|
end
|
|
|
|
defp apply_action(socket, :new, _params) do
|
|
coverage = %Coverage{
|
|
organization_id: socket.assigns.organization.id,
|
|
downtilt_deg: 0.0,
|
|
receiver_height_m: 3.0,
|
|
rx_threshold_dbm: -90.0,
|
|
cell_size_m: 10,
|
|
radius_m: 5_000
|
|
}
|
|
|
|
socket
|
|
|> assign(:page_title, t("New Coverage"))
|
|
|> assign(:coverage, coverage)
|
|
|> assign(:form, to_form(Coverages.change_coverage(coverage)))
|
|
end
|
|
|
|
defp apply_action(socket, :edit, %{"id" => id}) do
|
|
coverage = Coverages.get_coverage!(socket.assigns.organization.id, id)
|
|
|
|
socket
|
|
|> assign(:page_title, t("Edit Coverage"))
|
|
|> assign(:coverage, coverage)
|
|
|> assign(:form, to_form(Coverages.change_coverage(coverage)))
|
|
end
|
|
|
|
@impl true
|
|
def handle_event("validate", %{"coverage" => attrs}, socket) do
|
|
changeset =
|
|
socket.assigns.coverage
|
|
|> Coverages.change_coverage(attrs)
|
|
|> Map.put(:action, :validate)
|
|
|
|
{:noreply, assign(socket, :form, to_form(changeset))}
|
|
end
|
|
|
|
@impl true
|
|
def handle_event("save", %{"coverage" => attrs}, socket) do
|
|
save(socket, socket.assigns.live_action, attrs)
|
|
end
|
|
|
|
defp save(socket, :new, attrs) do
|
|
case Coverages.create_coverage(socket.assigns.organization.id, attrs) do
|
|
{:ok, coverage} ->
|
|
{:noreply,
|
|
socket
|
|
|> put_flash(:info, t("Coverage created"))
|
|
|> push_navigate(to: ~p"/coverage/#{coverage.id}")}
|
|
|
|
{:error, %Ecto.Changeset{} = changeset} ->
|
|
{:noreply, assign(socket, :form, to_form(changeset))}
|
|
end
|
|
end
|
|
|
|
defp save(socket, :edit, attrs) do
|
|
case Coverages.update_coverage(socket.assigns.coverage, attrs) do
|
|
{:ok, coverage} ->
|
|
{:noreply,
|
|
socket
|
|
|> put_flash(:info, t("Coverage updated"))
|
|
|> push_navigate(to: ~p"/coverage/#{coverage.id}")}
|
|
|
|
{:error, %Ecto.Changeset{} = changeset} ->
|
|
{:noreply, assign(socket, :form, to_form(changeset))}
|
|
end
|
|
end
|
|
|
|
defp antenna_options(antennas) do
|
|
antennas
|
|
|> Enum.group_by(& &1.manufacturer)
|
|
|> Enum.sort_by(fn {mfr, _} -> mfr || "" end)
|
|
|> Enum.map(fn {mfr, ants} ->
|
|
{mfr || t("Other"),
|
|
Enum.map(ants, fn ant ->
|
|
{"#{ant.model} (#{ant.gain_dbi} dBi)", ant.slug}
|
|
end)}
|
|
end)
|
|
end
|
|
end
|