The hidden 'false' shadow input was rendered after the checkbox, so when both were submitted the 'false' value overwrote the checkbox 'true' in the resulting params map. Move the hidden input before the checkbox so the checked value wins (matching the core_components checkbox pattern).
299 lines
8.8 KiB
Elixir
299 lines
8.8 KiB
Elixir
defmodule MicrowavepropWeb.RoverPlanningLive.Form do
|
|
@moduledoc "New / edit form for `/rover-planning` missions."
|
|
use MicrowavepropWeb, :live_view
|
|
|
|
alias Microwaveprop.Accounts.User
|
|
alias Microwaveprop.RoverPlanning
|
|
alias Microwaveprop.RoverPlanning.Mission
|
|
alias Microwaveprop.RoverPlanning.Station
|
|
|
|
@band_options [
|
|
{"902 MHz", 902},
|
|
{"1.3 GHz", 1296},
|
|
{"2.3 GHz", 2304},
|
|
{"3.4 GHz", 3400},
|
|
{"5.7 GHz", 5760},
|
|
{"10 GHz", 10_000},
|
|
{"24 GHz", 24_000},
|
|
{"47 GHz", 47_000}
|
|
]
|
|
|
|
@impl true
|
|
def mount(params, _session, socket) do
|
|
case current_user(socket) do
|
|
nil ->
|
|
{:ok,
|
|
socket
|
|
|> put_flash(:error, "Sign in to plan a mission.")
|
|
|> push_navigate(to: ~p"/rover-planning")}
|
|
|
|
%User{} = user ->
|
|
{:ok,
|
|
socket
|
|
|> assign(
|
|
page_title: "Rover Mission",
|
|
band_options: @band_options,
|
|
current_user: user
|
|
)
|
|
|> apply_action(socket.assigns.live_action, params)}
|
|
end
|
|
end
|
|
|
|
defp apply_action(socket, :new, _params) do
|
|
mission = %Mission{stations: [%Station{position: 0}]}
|
|
cs = Mission.changeset(mission, %{})
|
|
|
|
assign(socket,
|
|
mission: mission,
|
|
form: to_form(cs),
|
|
mode: :new
|
|
)
|
|
end
|
|
|
|
defp apply_action(socket, :edit, %{"id" => id}) do
|
|
user = socket.assigns.current_user
|
|
|
|
case RoverPlanning.get_mission(id) do
|
|
nil ->
|
|
socket
|
|
|> put_flash(:error, "Mission not found.")
|
|
|> push_navigate(to: ~p"/rover-planning")
|
|
|
|
mission ->
|
|
if can_modify?(user, mission) do
|
|
cs = Mission.changeset(mission, %{})
|
|
|
|
assign(socket,
|
|
mission: mission,
|
|
form: to_form(cs),
|
|
mode: :edit
|
|
)
|
|
else
|
|
socket
|
|
|> put_flash(:error, "You can only edit your own missions.")
|
|
|> push_navigate(to: ~p"/rover-planning/#{mission.id}")
|
|
end
|
|
end
|
|
end
|
|
|
|
@impl true
|
|
def handle_event("validate", %{"mission" => params}, socket) do
|
|
cs =
|
|
socket.assigns.mission
|
|
|> Mission.changeset(params)
|
|
|> Map.put(:action, :validate)
|
|
|
|
{:noreply, assign(socket, form: to_form(cs))}
|
|
end
|
|
|
|
def handle_event("add_station", _params, socket) do
|
|
params = current_params(socket)
|
|
stations = Map.get(params, "stations", %{})
|
|
next_index = next_station_index(stations)
|
|
|
|
stations =
|
|
Map.put(stations, Integer.to_string(next_index), %{
|
|
"input" => "",
|
|
"position" => Integer.to_string(next_index)
|
|
})
|
|
|
|
cs =
|
|
socket.assigns.mission
|
|
|> Mission.changeset(Map.put(params, "stations", stations))
|
|
|> Map.put(:action, :validate)
|
|
|
|
{:noreply, assign(socket, form: to_form(cs))}
|
|
end
|
|
|
|
def handle_event("remove_station", %{"index" => index}, socket) do
|
|
params = current_params(socket)
|
|
stations = Map.get(params, "stations", %{})
|
|
stations = Map.delete(stations, index)
|
|
|
|
cs =
|
|
socket.assigns.mission
|
|
|> Mission.changeset(Map.put(params, "stations", stations))
|
|
|> Map.put(:action, :validate)
|
|
|
|
{:noreply, assign(socket, form: to_form(cs))}
|
|
end
|
|
|
|
def handle_event("save", %{"mission" => params}, socket) do
|
|
user = socket.assigns.current_user
|
|
|
|
result =
|
|
case socket.assigns.mode do
|
|
:new -> RoverPlanning.create_mission(user, params)
|
|
:edit -> RoverPlanning.update_mission(user, socket.assigns.mission.id, params)
|
|
end
|
|
|
|
case result do
|
|
{:ok, mission} ->
|
|
{:noreply,
|
|
socket
|
|
|> put_flash(:info, "Mission saved — computing path profiles.")
|
|
|> push_navigate(to: ~p"/rover-planning/#{mission.id}")}
|
|
|
|
{:error, :not_found} ->
|
|
{:noreply, put_flash(socket, :error, "Mission not found.")}
|
|
|
|
{:error, %Ecto.Changeset{} = cs} ->
|
|
{:noreply, assign(socket, form: to_form(cs))}
|
|
end
|
|
end
|
|
|
|
defp current_params(socket) do
|
|
case socket.assigns.form.source do
|
|
%Ecto.Changeset{params: params} when is_map(params) -> params
|
|
_ -> %{}
|
|
end
|
|
end
|
|
|
|
defp next_station_index(stations) when map_size(stations) == 0, do: 0
|
|
|
|
defp next_station_index(stations) do
|
|
stations
|
|
|> Map.keys()
|
|
|> Enum.map(fn k ->
|
|
case Integer.parse(to_string(k)) do
|
|
{n, _} -> n
|
|
:error -> -1
|
|
end
|
|
end)
|
|
|> Enum.max()
|
|
|> Kernel.+(1)
|
|
end
|
|
|
|
defp current_user(%Phoenix.LiveView.Socket{assigns: assigns}) do
|
|
case assigns[:current_scope] do
|
|
%{user: %User{} = user} -> user
|
|
_ -> nil
|
|
end
|
|
end
|
|
|
|
defp can_modify?(%User{is_admin: true}, _), do: true
|
|
defp can_modify?(%User{id: id}, %Mission{user_id: id}) when not is_nil(id), do: true
|
|
defp can_modify?(_, _), do: false
|
|
|
|
@impl true
|
|
def render(assigns) do
|
|
~H"""
|
|
<Layouts.app flash={@flash} current_scope={@current_scope} max_width="max-w-3xl">
|
|
<.header>
|
|
{if @mode == :edit, do: "Edit mission", else: "New rover mission"}
|
|
<:subtitle>
|
|
Stationary stations can be callsigns, Maidenhead grids, or <code>lat, lon</code> pairs.
|
|
</:subtitle>
|
|
</.header>
|
|
|
|
<.form
|
|
for={@form}
|
|
id="mission-form"
|
|
phx-change="validate"
|
|
phx-submit="save"
|
|
class="space-y-4"
|
|
>
|
|
<.input field={@form[:name]} type="text" label="Mission name" placeholder="DFW August rove" />
|
|
|
|
<div class="grid grid-cols-1 md:grid-cols-3 gap-3">
|
|
<.input
|
|
field={@form[:band_mhz]}
|
|
type="select"
|
|
label="Band"
|
|
options={@band_options}
|
|
/>
|
|
<.input
|
|
field={@form[:rover_height_ft]}
|
|
type="number"
|
|
step="any"
|
|
label="Rover antenna (ft)"
|
|
/>
|
|
<.input
|
|
field={@form[:station_height_ft]}
|
|
type="number"
|
|
step="any"
|
|
label="Station antenna (ft)"
|
|
/>
|
|
</div>
|
|
|
|
<label class="flex items-center gap-2 cursor-pointer">
|
|
<input type="hidden" name="mission[only_known_good]" value="false" />
|
|
<input
|
|
type="checkbox"
|
|
name="mission[only_known_good]"
|
|
value="true"
|
|
checked={Phoenix.HTML.Form.normalize_value("checkbox", @form[:only_known_good].value)}
|
|
class="checkbox checkbox-primary"
|
|
/>
|
|
<span>
|
|
Only check against known good locations
|
|
<span class="text-xs opacity-70 block">
|
|
When checked, paths are computed only against rover-locations marked Good.
|
|
</span>
|
|
</span>
|
|
</label>
|
|
|
|
<div>
|
|
<div class="flex items-center justify-between mb-2">
|
|
<h3 class="font-semibold">Stationary stations</h3>
|
|
<button
|
|
type="button"
|
|
phx-click="add_station"
|
|
class="btn btn-xs btn-ghost"
|
|
>
|
|
<.icon name="hero-plus" class="w-4 h-4" /> Add station
|
|
</button>
|
|
</div>
|
|
|
|
<.inputs_for :let={s} field={@form[:stations]}>
|
|
<div class="card bg-base-200 p-3 mb-2">
|
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-2">
|
|
<.input
|
|
field={s[:input]}
|
|
type="text"
|
|
label="Callsign / grid / lat,lon"
|
|
placeholder="W5LUA or EM13qc or 32.91, -97.06"
|
|
/>
|
|
<div class="flex items-end gap-2">
|
|
<div class="flex-1">
|
|
<.input field={s[:lat]} type="number" step="any" label="Lat" />
|
|
</div>
|
|
<div class="flex-1">
|
|
<.input field={s[:lon]} type="number" step="any" label="Lon" />
|
|
</div>
|
|
<button
|
|
type="button"
|
|
phx-click="remove_station"
|
|
phx-value-index={s.index}
|
|
class="btn btn-ghost btn-sm text-error mb-2"
|
|
title="Remove station"
|
|
>
|
|
<.icon name="hero-trash" class="w-4 h-4" />
|
|
</button>
|
|
</div>
|
|
</div>
|
|
<p :if={s[:callsign].value} class="text-xs opacity-70 mt-1 px-1">
|
|
Resolved callsign: {s[:callsign].value}
|
|
<span :if={s[:grid].value}>· grid {s[:grid].value}</span>
|
|
</p>
|
|
</div>
|
|
</.inputs_for>
|
|
|
|
<p :if={@form[:stations].errors != []} class="text-error text-sm">
|
|
{Phoenix.HTML.html_escape(Enum.map_join(@form[:stations].errors, ", ", &elem(&1, 0)))}
|
|
</p>
|
|
</div>
|
|
|
|
<.input field={@form[:notes]} type="textarea" label="Notes" rows="3" />
|
|
|
|
<div class="flex gap-2">
|
|
<button type="submit" class="btn btn-primary">
|
|
{if @mode == :edit, do: "Save changes", else: "Create mission"}
|
|
</button>
|
|
<.link navigate={~p"/rover-planning"} class="btn btn-ghost">Cancel</.link>
|
|
</div>
|
|
</.form>
|
|
</Layouts.app>
|
|
"""
|
|
end
|
|
end
|