prop/lib/microwaveprop_web/live/beacon_live/form.ex
Graham McIntire ff950f9e40
docs/tests: moduledocs on 18 LiveViews, MapLive event coverage, Rust pipeline roundtrip
- Add one-line moduledocs to 18 LiveViews that were carrying
  @moduledoc false. Each line names the route and summarizes what
  the page does so a future reader knows where to look before
  opening the file.
- Add MapLive handle_event tests for toggle_radar, select_time /
  set_selected_time, point_detail, map_bounds, retry_initial_scores.
  Assertions focus on socket-state transitions and "didn't crash"
  rather than raw HTML — map_bounds in particular has no visible
  render-side effect (it push_events the new scores to the JS hook).
- Rust pipeline: integration-shaped test that hand-builds a 2-cell
  surface + pressure grid, runs merge → cell_to_conditions →
  precompute_band_invariants → composite_score_with → write_atomic
  → decode, asserting header + body length round-trip. Closes the
  'pipeline's happy path is only covered by unit tests' gap.
2026-04-21 17:16:12 -05:00

166 lines
5 KiB
Elixir

defmodule MicrowavepropWeb.BeaconLive.Form do
@moduledoc "Shared new/edit form for beacon records, used by `BeaconLive.Index`."
use MicrowavepropWeb, :live_view
alias Microwaveprop.Beacons
alias Microwaveprop.Beacons.Beacon
@impl true
def render(assigns) do
~H"""
<Layouts.app flash={@flash} current_scope={@current_scope}>
<.header>
{@page_title}
<:subtitle>
Enter either a grid or lat/lon — whichever you leave blank is filled in
from the other.
</:subtitle>
</.header>
<.form for={@form} id="beacon-form" phx-change="validate" phx-submit="save">
<.input
field={@form[:frequency_mhz]}
type="text"
label="Frequency (MHz)"
inputmode="decimal"
phx-hook="CommaNumber"
required
/>
<.input field={@form[:callsign]} type="text" label="Call" required />
<.input field={@form[:grid]} type="text" label="Grid" />
<.input field={@form[:lat]} type="number" label="Latitude" step="any" />
<.input field={@form[:lon]} type="number" label="Longitude" step="any" />
<.input
field={@form[:power_mw]}
type="number"
label="TX power (EIRP) (mW)"
step="any"
required
/>
<.input
field={@form[:height_ft]}
type="number"
label="Height above ground (ft)"
required
/>
<.input
field={@form[:bearing]}
type="text"
label="Bearing"
placeholder="omni or 0-360 degrees"
/>
<.input
field={@form[:beamwidth_deg]}
type="number"
label="Beamwidth (degrees)"
step="any"
placeholder="Leave blank for omni"
/>
<.input
field={@form[:keying]}
type="select"
label="Keying"
options={Beacon.keying_options()}
required
/>
<.input field={@form[:on_the_air]} type="checkbox" label="On the air" />
<.input field={@form[:notes]} type="textarea" label="Notes" />
<footer>
<.button phx-disable-with="Saving..." variant="primary">Save Beacon</.button>
<.button navigate={return_path(@return_to, @beacon)}>Cancel</.button>
</footer>
</.form>
</Layouts.app>
"""
end
@impl true
def mount(params, _session, socket) do
{:ok,
socket
|> assign(:return_to, return_to(params["return_to"]))
|> apply_action(socket.assigns.live_action, params)}
end
defp return_to("show"), do: "show"
defp return_to(_), do: "index"
defp apply_action(socket, :edit, %{"id" => id}) do
beacon = Beacons.get_beacon!(id)
socket
|> assign(:page_title, "Edit beacon")
|> assign(:beacon, beacon)
|> assign(:form, to_form(Beacons.change_beacon(beacon)))
end
defp apply_action(socket, :new, _params) do
beacon = %Beacon{}
socket
|> assign(:page_title, "New beacon")
|> assign(:beacon, beacon)
|> assign(:form, to_form(Beacons.change_beacon(beacon)))
end
@impl true
def handle_event("validate", %{"beacon" => beacon_params}, socket) do
changeset = Beacons.change_beacon(socket.assigns.beacon, strip_commas(beacon_params))
{:noreply, assign(socket, form: to_form(changeset, action: :validate))}
end
def handle_event("save", %{"beacon" => beacon_params}, socket) do
save_beacon(socket, socket.assigns.live_action, strip_commas(beacon_params))
end
defp strip_commas(params) do
params
|> Map.update("frequency_mhz", nil, fn
v when is_binary(v) -> String.replace(v, ",", "")
v -> v
end)
|> Map.update("height_ft", nil, fn
v when is_binary(v) -> String.replace(v, ~r/\.0*$/, "")
v -> v
end)
end
defp save_beacon(socket, :edit, beacon_params) do
case Beacons.update_beacon(socket.assigns.beacon, beacon_params) do
{:ok, beacon} ->
{:noreply,
socket
|> put_flash(:info, "Beacon updated")
|> push_navigate(to: return_path(socket.assigns.return_to, beacon))}
{:error, %Ecto.Changeset{} = changeset} ->
{:noreply, assign(socket, form: to_form(changeset))}
end
end
defp save_beacon(socket, :new, beacon_params) do
user = current_user(socket.assigns.current_scope)
case Beacons.create_beacon(user, beacon_params) do
{:ok, beacon} ->
flash =
if user,
do: "Beacon submitted — pending admin approval.",
else: "Thanks! Your beacon has been submitted and is awaiting admin approval."
{:noreply,
socket
|> put_flash(:info, flash)
|> push_navigate(to: return_path(socket.assigns.return_to, beacon))}
{:error, %Ecto.Changeset{} = changeset} ->
{:noreply, assign(socket, form: to_form(changeset))}
end
end
defp current_user(%{user: user}), do: user
defp current_user(_), do: nil
defp return_path("index", _beacon), do: ~p"/beacons"
defp return_path("show", beacon), do: ~p"/beacons/#{beacon}"
end