prop/lib/microwaveprop_web/live/beacon_live/form.ex
Graham McIntire b4b8d4ec47
Some checks failed
Build base image / Build and push base image (push) Successful in 12s
Build and Push / Build CI test image (push) Successful in 14s
Build and Push / Build and Push Docker Image (push) Failing after 14m7s
simplify: DRY up shared changesets, context helpers, LiveView helpers, and structural extraction
- Create MaidenheadChangesetHelpers: consolidate grid validation, callsign
  normalization, lat/lon validation, grid/latlon derivation across 6 schemas
- Create ContextHelpers: shared fetch_owned with admin bypass, safe_enqueue
  for Oban workers, UUID casting to replace CastError rescues
- Extend LiveHelpers: add current_user/1 (removes 7 duplicate definitions),
  subscribe/2 (replaces 13 inline PubSub sites), assign_url_params/2
- Extract Propagation.ScoreStore (528 lines): separate file I/O and cache
  management from scoring logic, 13 defdelegate passthroughs
- Split SubmitLive (942->475 lines): extract CSV/ADIF upload rendering into
  3 function component modules (csv_upload, adif_upload, preview)
- Update 16 LiveViews to use shared helpers
2026-08-06 18:06:50 -05:00

165 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
import MicrowavepropWeb.LiveHelpers, only: [current_user: 1]
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 return_path("index", _beacon), do: ~p"/beacons"
defp return_path("show", beacon), do: ~p"/beacons/#{beacon}"
end