defmodule MicrowavepropWeb.BeaconLive.Index do @moduledoc false use MicrowavepropWeb, :live_view alias Microwaveprop.Beacons alias Microwaveprop.Beacons.Beacon @impl true def render(assigns) do ~H""" <.header> Beacons <:subtitle>Microwave beacons tracked by NTMS. <:actions> <.button variant="primary" navigate={~p"/beacons/new"}> <.icon name="hero-plus" /> Submit Beacon
<.table id="beacons" rows={@streams.beacons} row_click={fn {_id, beacon} -> JS.navigate(~p"/beacons/#{beacon}") end} > <:col :let={{_id, beacon}} label="Frequency (MHz)"> {Beacon.format_freq(beacon.frequency_mhz)} <:col :let={{_id, beacon}} label="Call">{beacon.callsign} <:col :let={{_id, beacon}} label="Grid">{beacon.grid} <:col :let={{_id, beacon}} label="EIRP (mW)">{Beacon.format_mw(beacon.power_mw)} <:col :let={{_id, beacon}} label="Height AGL (ft)">{beacon.height_ft} <:col :let={{_id, beacon}} label="Keying">{Beacon.keying_label(beacon.keying)} <:col :let={{_id, beacon}} label="On air"> {if beacon.on_the_air, do: "Yes", else: "No"} <:action :let={{_id, beacon}}>
<.link navigate={~p"/beacons/#{beacon}"}>Show
<.link :if={admin?(@current_scope)} navigate={~p"/beacons/#{beacon}/edit"}>Edit <:action :let={{id, beacon}}> <.link :if={admin?(@current_scope)} phx-click={JS.push("delete", value: %{id: beacon.id}) |> hide("##{id}")} data-confirm="Delete this beacon?" > Delete
<.header> Pending approval <:subtitle>Submitted beacons waiting for admin review. <.table id="pending-beacons" rows={@streams.pending} row_click={fn {_id, beacon} -> JS.navigate(~p"/beacons/#{beacon}") end} > <:col :let={{_id, beacon}} label="Frequency (MHz)"> {Beacon.format_freq(beacon.frequency_mhz)} <:col :let={{_id, beacon}} label="Call">{beacon.callsign} <:col :let={{_id, beacon}} label="Grid">{beacon.grid} <:col :let={{_id, beacon}} label="EIRP (mW)">{Beacon.format_mw(beacon.power_mw)} <:col :let={{_id, beacon}} label="Height AGL (ft)">{beacon.height_ft} <:col :let={{_id, beacon}} label="Submitted"> {Calendar.strftime(beacon.inserted_at, "%Y-%m-%d %H:%M UTC")} <:action :let={{_id, beacon}}> <.link phx-click={JS.push("approve", value: %{id: beacon.id})} data-confirm="Approve this beacon?" > Approve <:action :let={{id, beacon}}> <.link phx-click={JS.push("delete", value: %{id: beacon.id}) |> hide("##{id}")} data-confirm="Delete this beacon?" > Delete
""" end @impl true def mount(_params, _session, socket) do if connected?(socket), do: Beacons.subscribe_beacons() beacons = Beacons.list_beacons() pending = if admin?(socket.assigns.current_scope) do Beacons.list_pending_beacons() else [] end {:ok, socket |> assign(:page_title, "Beacons") |> assign(:pending, pending) |> assign(:beacons_json, encode_beacons(beacons)) |> stream(:beacons, beacons) |> stream(:pending, pending)} end @impl true def handle_event("delete", %{"id" => id}, socket) do if admin?(socket.assigns.current_scope) do beacon = Beacons.get_beacon!(id) {:ok, _} = Beacons.delete_beacon(beacon) {:noreply, socket |> stream_delete(:beacons, beacon) |> stream_delete(:pending, beacon)} else {:noreply, put_flash(socket, :error, "Admins only.")} end end def handle_event("approve", %{"id" => id}, socket) do if admin?(socket.assigns.current_scope) do beacon = Beacons.get_beacon!(id) {:ok, approved} = Beacons.approve_beacon(beacon) {:noreply, socket |> put_flash(:info, "Approved #{approved.callsign}.") |> stream_delete(:pending, beacon) |> stream_insert(:beacons, approved)} else {:noreply, put_flash(socket, :error, "Admins only.")} end end @impl true def handle_info({type, %Beacon{}}, socket) when type in [:created, :updated, :deleted] do beacons = Beacons.list_beacons() pending = if admin?(socket.assigns.current_scope) do Beacons.list_pending_beacons() else [] end {:noreply, socket |> assign(:pending, pending) |> assign(:beacons_json, encode_beacons(beacons)) |> stream(:beacons, beacons, reset: true) |> stream(:pending, pending, reset: true)} end defp admin?(%{user: %{is_admin: true}}), do: true defp admin?(_), do: false # Serialize just the fields the map hook needs. `phx-update="ignore"` # means this data attribute is only read on initial hook mount — the # map won't re-render when beacons change without a full LV reload. # For now that's fine because list churn is rare; upgrade to # push_event("update_beacons", ...) if we start getting live PubSub # updates that need to reflect on the map without a refresh. defp encode_beacons(beacons) do beacons |> Enum.filter(&(is_number(&1.lat) and is_number(&1.lon))) |> Enum.map(fn b -> %{ id: b.id, callsign: b.callsign, lat: b.lat, lon: b.lon, frequency_mhz: b.frequency_mhz, grid: b.grid, keying: Beacon.keying_label(b.keying), on_the_air: b.on_the_air } end) |> Jason.encode!() end end