feat(rover-planning): warn when new site overlaps an existing 6-char grid
The add-rover-site form now resolves the input on every change and, if any existing rover-location shares the same 6-char Maidenhead cell (~5 km), surfaces a 'Already saved as' link to that location's detail page. Avoids users accidentally duplicating a site they (or another operator) already added.
This commit is contained in:
parent
23d002e8e0
commit
b247e8a719
2 changed files with 96 additions and 3 deletions
|
|
@ -8,6 +8,7 @@ defmodule MicrowavepropWeb.RoverPlanningLive.Show do
|
|||
|
||||
alias Microwaveprop.Accounts.User
|
||||
alias Microwaveprop.Radio.Maidenhead
|
||||
alias Microwaveprop.Repo
|
||||
alias Microwaveprop.Rover
|
||||
alias Microwaveprop.Rover.Location
|
||||
alias Microwaveprop.RoverPlanning
|
||||
|
|
@ -35,7 +36,8 @@ defmodule MicrowavepropWeb.RoverPlanningLive.Show do
|
|||
mission: mission,
|
||||
paths: RoverPlanning.list_paths(mission),
|
||||
rover_sites: RoverPlanning.candidate_rover_locations(mission),
|
||||
rover_site_input: ""
|
||||
rover_site_input: "",
|
||||
rover_site_input_matches: []
|
||||
)}
|
||||
end
|
||||
end
|
||||
|
|
@ -47,7 +49,11 @@ defmodule MicrowavepropWeb.RoverPlanningLive.Show do
|
|||
|
||||
@impl true
|
||||
def handle_event("rover_site_input_change", %{"input" => input}, socket) do
|
||||
{:noreply, assign(socket, rover_site_input: input)}
|
||||
{:noreply,
|
||||
assign(socket,
|
||||
rover_site_input: input,
|
||||
rover_site_input_matches: find_grid_matches(input)
|
||||
)}
|
||||
end
|
||||
|
||||
def handle_event("add_rover_site", %{"input" => input}, socket) do
|
||||
|
|
@ -97,7 +103,8 @@ defmodule MicrowavepropWeb.RoverPlanningLive.Show do
|
|||
|> assign(
|
||||
rover_sites: RoverPlanning.candidate_rover_locations(mission),
|
||||
paths: RoverPlanning.list_paths(mission),
|
||||
rover_site_input: ""
|
||||
rover_site_input: "",
|
||||
rover_site_input_matches: []
|
||||
)
|
||||
|> put_flash(:info, flash_message)
|
||||
end
|
||||
|
|
@ -106,6 +113,30 @@ defmodule MicrowavepropWeb.RoverPlanningLive.Show do
|
|||
|
||||
defp authenticated(_), do: {:error, :unauthenticated}
|
||||
|
||||
# Resolve the user-typed input and surface any existing rover-locations
|
||||
# that share its 6-char Maidenhead grid (sub-grid resolution ≈ 5 km).
|
||||
# Cheap enough for ~hundreds of locations: we map them in memory rather
|
||||
# than maintaining a stored grid column.
|
||||
defp find_grid_matches(input) when is_binary(input) do
|
||||
trimmed = String.trim(input)
|
||||
|
||||
case trimmed != "" && LocationResolver.resolve(trimmed) do
|
||||
{:ok, %{lat: lat, lon: lon}} when is_number(lat) and is_number(lon) ->
|
||||
target = Maidenhead.from_latlon(lat, lon, 6)
|
||||
|
||||
Location
|
||||
|> Repo.all()
|
||||
|> Enum.filter(fn loc ->
|
||||
Maidenhead.from_latlon(loc.lat, loc.lon, 6) == target
|
||||
end)
|
||||
|
||||
_ ->
|
||||
[]
|
||||
end
|
||||
end
|
||||
|
||||
defp find_grid_matches(_), do: []
|
||||
|
||||
defp can_modify?(%{current_scope: %{user: %User{is_admin: true}}}, _), do: true
|
||||
|
||||
defp can_modify?(%{current_scope: %{user: %User{id: id}}}, %Mission{user_id: id}) when not is_nil(id), do: true
|
||||
|
|
@ -407,6 +438,18 @@ defmodule MicrowavepropWeb.RoverPlanningLive.Show do
|
|||
</button>
|
||||
</form>
|
||||
|
||||
<div :if={@rover_site_input_matches != []} class="mt-2 text-xs text-warning">
|
||||
Already saved as:
|
||||
<.link
|
||||
:for={match <- @rover_site_input_matches}
|
||||
navigate={~p"/rover-locations/#{match.id}"}
|
||||
class="link link-warning font-mono ml-1"
|
||||
>
|
||||
{Maidenhead.from_latlon(match.lat, match.lon, 6)}{if match.notes && match.notes != "",
|
||||
do: " (#{match.notes})"}
|
||||
</.link>
|
||||
</div>
|
||||
|
||||
<p :if={!(@current_scope && @current_scope.user)} class="text-xs text-base-content/60">
|
||||
<.link navigate={~p"/users/log-in"} class="link">Sign in</.link>
|
||||
to add or remove rover sites.
|
||||
|
|
|
|||
|
|
@ -409,6 +409,56 @@ defmodule MicrowavepropWeb.RoverPlanningLiveTest do
|
|||
assert Repo.aggregate(Location, :count) == 2
|
||||
end
|
||||
|
||||
test "typing a new rover site surfaces an existing match by 6-char grid",
|
||||
%{conn: conn} do
|
||||
user = AccountsFixtures.user_fixture()
|
||||
mission = create_mission(user, "Match-existing mission")
|
||||
conn = log_in_user(conn, user)
|
||||
|
||||
# Existing site at a known grid; the user-typed coords below
|
||||
# land in the same 6-char Maidenhead cell, so the form should
|
||||
# surface a link to the existing site rather than letting the
|
||||
# user duplicate it.
|
||||
{:ok, existing} =
|
||||
Rover.create_location(user, %{
|
||||
lat: 32.5,
|
||||
lon: -97.5,
|
||||
status: :good,
|
||||
notes: "Existing parking spot"
|
||||
})
|
||||
|
||||
{:ok, lv, _html} = live(conn, ~p"/rover-planning/#{mission.id}")
|
||||
|
||||
html =
|
||||
lv
|
||||
|> form("form[phx-submit='add_rover_site']", %{"input" => "32.5004, -97.4998"})
|
||||
|> render_change()
|
||||
|
||||
# The 6-char grid for both points is identical, so a match link
|
||||
# to the existing site appears with its grid label.
|
||||
assert html =~ ~s(href="/rover-locations/#{existing.id})
|
||||
grid6 = Maidenhead.from_latlon(32.5, -97.5, 6)
|
||||
assert html =~ grid6
|
||||
end
|
||||
|
||||
test "non-matching input shows no existing-site link", %{conn: conn} do
|
||||
user = AccountsFixtures.user_fixture()
|
||||
mission = create_mission(user, "No-match mission")
|
||||
conn = log_in_user(conn, user)
|
||||
|
||||
{:ok, _other} =
|
||||
Rover.create_location(user, %{lat: 32.5, lon: -97.5, status: :good})
|
||||
|
||||
{:ok, lv, _html} = live(conn, ~p"/rover-planning/#{mission.id}")
|
||||
|
||||
html =
|
||||
lv
|
||||
|> form("form[phx-submit='add_rover_site']", %{"input" => "45.0, -120.0"})
|
||||
|> render_change()
|
||||
|
||||
refute html =~ "Already saved as"
|
||||
end
|
||||
|
||||
test "owner can remove a rover site from the show page", %{conn: conn} do
|
||||
user = AccountsFixtures.user_fixture()
|
||||
mission = create_mission(user, "Remove-site mission")
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue