diff --git a/lib/microwaveprop/beacons.ex b/lib/microwaveprop/beacons.ex index 91cfdbb9..99d1f615 100644 --- a/lib/microwaveprop/beacons.ex +++ b/lib/microwaveprop/beacons.ex @@ -39,6 +39,15 @@ defmodule Microwaveprop.Beacons do ) end + @doc """ + Base Ecto query for approved beacons. Used as a `live_table` + `data_provider` so sort/search/pagination can be applied on top. + """ + @spec approved_beacons_query() :: Ecto.Query.t() + def approved_beacons_query do + from b in Beacon, as: :resource, where: b.approved == true + end + @doc "Returns unapproved beacons awaiting admin review." @spec list_pending_beacons() :: [Beacon.t()] def list_pending_beacons do diff --git a/lib/microwaveprop_web/live/beacon_live/index.ex b/lib/microwaveprop_web/live/beacon_live/index.ex index bfe67450..2130b087 100644 --- a/lib/microwaveprop_web/live/beacon_live/index.ex +++ b/lib/microwaveprop_web/live/beacon_live/index.ex @@ -2,9 +2,155 @@ defmodule MicrowavepropWeb.BeaconLive.Index do @moduledoc false use MicrowavepropWeb, :live_view + use LiveTable.LiveResource, schema: Microwaveprop.Beacons.Beacon + alias Microwaveprop.Beacons alias Microwaveprop.Beacons.Beacon + def fields do + [ + id: %{label: "ID", hidden: true}, + frequency_mhz: %{ + label: "Frequency (MHz)", + sortable: true, + searchable: false, + renderer: &freq_cell/1 + }, + callsign: %{label: "Call", sortable: true, searchable: true}, + grid: %{label: "Grid", sortable: true, searchable: true}, + power_mw: %{label: "EIRP (mW)", sortable: true, renderer: &mw_cell/1}, + height_ft: %{label: "Height AGL (ft)", sortable: true}, + keying: %{label: "Keying", sortable: true, renderer: &keying_cell/1}, + on_the_air: %{label: "On air", sortable: true, renderer: &on_air_cell/1} + ] + end + + def filters, do: [] + + def actions do + [ + show: fn %{record: beacon} -> + assigns = %{beacon: beacon} + + ~H""" + <.link navigate={~p"/beacons/#{@beacon.id}"} class="btn btn-xs btn-ghost">Details + """ + end + ] + 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)) + |> assign(:data_provider, {Microwaveprop.Beacons, :approved_beacons_query, []}) + |> 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(:pending, beacon) + |> push_patch(to: socket.assigns.current_path |> path_with_prefix())} + 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) + |> push_patch(to: socket.assigns.current_path |> path_with_prefix())} + 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(:pending, pending, reset: true) + |> push_patch(to: socket.assigns.current_path |> path_with_prefix())} + end + + defp admin?(%{user: %{is_admin: true}}), do: true + defp admin?(_), do: false + + defp path_with_prefix(path) when is_binary(path), do: "/" <> path + defp path_with_prefix(_), do: "/beacons" + + 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 + + defp freq_cell(freq), do: Beacon.format_freq(freq) + defp mw_cell(power), do: Beacon.format_mw(power) + defp keying_cell(keying), do: Beacon.keying_label(keying) + + defp on_air_cell(on_the_air) do + assigns = %{on_the_air: on_the_air} + + ~H""" + + {if @on_the_air, do: "Yes", else: "No"} + + """ + end + @impl true def render(assigns) do ~H""" @@ -28,40 +174,13 @@ defmodule MicrowavepropWeb.BeaconLive.Index do > - <.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}}> -