prop/lib/microwaveprop_web/live/contact_map_live.ex
Graham McIntire a66d3094ca Add contact edit approval system with admin review queue
Registered users can suggest edits to any contact's core fields
(callsigns, grids, band, mode, timestamp). Edits enter an admin
approval queue with field-by-field diff view. On approve, changes
are applied and enrichment re-enqueued if grids/band changed.
Users receive email notification on approve or reject.

Also updates dependabot.yml for mix ecosystem.
2026-04-11 16:15:49 -05:00

83 lines
2.3 KiB
Elixir

defmodule MicrowavepropWeb.ContactMapLive do
@moduledoc false
use MicrowavepropWeb, :live_view
import Ecto.Query
alias Microwaveprop.Radio.Contact
alias Microwaveprop.Repo
@impl true
def mount(_params, _session, socket) do
contacts = load_contacts()
{:ok,
assign(socket,
page_title: "Contact Map",
contacts_json: Jason.encode!(contacts),
contact_count: length(contacts)
)}
end
defp load_contacts do
from(c in Contact,
where: not is_nil(c.pos1) and not is_nil(c.pos2),
select: %{
id: c.id,
pos1: c.pos1,
pos2: c.pos2,
band: c.band,
station1: c.station1,
station2: c.station2,
mode: c.mode,
distance_km: c.distance_km,
qso_timestamp: c.qso_timestamp
}
)
|> Repo.all()
|> Enum.map(fn c ->
lat1 = c.pos1["lat"]
lon1 = c.pos1["lon"] || c.pos1["lng"]
lat2 = c.pos2["lat"]
lon2 = c.pos2["lon"] || c.pos2["lng"]
band = if c.band, do: Decimal.to_integer(c.band), else: 0
dist = if c.distance_km, do: Decimal.to_float(c.distance_km)
ts = if c.qso_timestamp, do: Calendar.strftime(c.qso_timestamp, "%Y-%m-%d %H:%M")
if lat1 && lon1 && lat2 && lon2 do
[lat1, lon1, lat2, lon2, band, c.station1, c.station2, c.mode, dist, ts, c.id]
end
end)
|> Enum.reject(&is_nil/1)
|> dedup_reciprocals()
end
# Deduplicate reciprocal contacts (A↔B same band/hour = same contact logged by both sides)
defp dedup_reciprocals(contacts) do
Enum.uniq_by(contacts, fn [_lat1, _lon1, _lat2, _lon2, band, s1, s2, _mode, _dist, ts, _id] ->
stations = Enum.sort([s1 || "", s2 || ""])
hour = if ts, do: String.slice(ts, 0..12), else: ""
{stations, band, hour}
end)
end
@impl true
def render(assigns) do
~H"""
<div
id="contacts-map"
phx-hook="ContactsMap"
phx-update="ignore"
class="fixed inset-0 z-0"
data-contacts={@contacts_json}
>
</div>
<div class="fixed top-3 left-14 z-[1000] bg-base-100 shadow rounded-box border border-base-300 px-3 py-2 flex flex-col gap-1">
<div class="font-bold text-sm">Contact Map</div>
<div id="contact-count" class="text-xs opacity-70">
{Integer.to_string(@contact_count)} contacts
</div>
</div>
"""
end
end