defmodule MicrowavepropWeb.RoverPlanningLive do @moduledoc """ `/rover-planning` paginated table of planned rover missions. Globally visible, owner-mutable; admins can edit/delete any mission. """ use MicrowavepropWeb, :live_view use MicrowavepropWeb.LiveTableResource, schema: Microwaveprop.RoverPlanning.Mission alias Microwaveprop.Accounts.User alias Microwaveprop.RoverPlanning alias Microwaveprop.RoverPlanning.Mission @spec table_options() :: map() def table_options do %{sorting: %{default_sort: [inserted_at: :desc]}} end @spec fields() :: keyword() def fields do [ id: %{label: "ID", hidden: true}, user_id: %{label: "Owner", hidden: true}, name: %{label: "Mission", sortable: true, searchable: true, renderer: &name_cell/2}, band_mhz: %{label: "Bands", sortable: true, renderer: &bands_cell/2}, only_known_good: %{label: "Scope", sortable: true, renderer: &scope_cell/1}, inserted_at: %{label: "Created", sortable: true, renderer: &format_ts/1} ] end @spec filters() :: list() def filters, do: [] @impl true def mount(_params, _session, socket) do {:ok, assign(socket, page_title: "Rover Planning" )} end @impl true def handle_event("delete", %{"id" => id}, socket) do case current_user(socket) do %User{} = user -> case RoverPlanning.delete_mission(user, id) do {:ok, _} -> {:noreply, socket |> put_flash(:info, "Mission deleted.") |> reload()} {:error, :not_found} -> {:noreply, put_flash(socket, :error, "You can only delete your own missions.")} end _ -> {:noreply, put_flash(socket, :error, "Sign in required.")} end end defp reload(socket) do options = socket.assigns.options data_provider = socket.assigns[:data_provider] table_options = get_merged_table_options() {resources, updated_options} = fetch_resources(options, data_provider) socket |> assign_to_socket(resources, table_options) |> assign(:options, updated_options) end defp current_user(%Phoenix.LiveView.Socket{assigns: assigns}), do: scope_user(assigns) defp current_user(assigns) when is_map(assigns), do: scope_user(assigns) defp scope_user(assigns) do case assigns[:current_scope] do %{user: %User{} = user} -> user _ -> nil end end defp can_modify?(scope, %{user_id: user_id}) do case scope do %{user: %User{is_admin: true}} -> true %{user: %User{id: ^user_id}} when not is_nil(user_id) -> true _ -> false end end defp can_modify?(_, _), do: false defp actions_for(scope) do [ buttons: fn %{record: record} -> row_actions(record, scope) end ] end defp row_actions(record, scope) do assigns = %{record: record, can_modify?: can_modify?(scope, record)} ~H"""
<.link navigate={~p"/rover-planning/#{@record.id}"} class="btn btn-xs btn-ghost" > View <.link :if={@can_modify?} navigate={~p"/rover-planning/#{@record.id}/edit"} class="btn btn-xs btn-ghost" > Edit
""" end defp name_cell(value, %{id: id}) do assigns = %{id: id, name: value || "(unnamed)"} ~H""" <.link navigate={~p"/rover-planning/#{@id}"} class="font-semibold hover:underline"> {@name} """ end # Renders the full bands list (multi-band missions can have several; # the bound column is `band_mhz` so the table can still sort by the # primary band, but the cell shows every band the matrix scores). defp bands_cell(_value, record) do case Mission.bands(struct(Mission, Map.take(record, [:band_mhz, :bands_mhz]))) do [] -> "—" [single] -> band_label(single) list -> Enum.map_join(list, " · ", &band_label/1) end end defp band_label(mhz) when is_integer(mhz) and mhz >= 1000 do ghz = mhz / 1000 label = if ghz == trunc(ghz), do: "#{trunc(ghz)}", else: Float.to_string(Float.round(ghz, 1)) "#{label} GHz" end defp band_label(mhz) when is_integer(mhz), do: "#{mhz} MHz" defp band_label(_), do: "—" defp scope_cell(true) do assigns = %{} ~H|Known good only| end defp scope_cell(false) do assigns = %{} ~H|All locations| end defp scope_cell(_), do: "" defp format_ts(nil), do: "" defp format_ts(%DateTime{} = dt), do: Calendar.strftime(dt, "%Y-%m-%d") defp format_ts(%NaiveDateTime{} = dt), do: Calendar.strftime(dt, "%Y-%m-%d") defp format_ts(other), do: to_string(other) @impl true def render(assigns) do ~H""" <.header> Rover Planning <:subtitle> Plan rover missions: enter your stationary stations, pick a band, and we compute terrain path profiles from every known-good rover location. <:actions> <.link :if={current_user(assigns)} navigate={~p"/rover-planning/new"} class="btn btn-primary" > <.icon name="hero-plus" class="w-5 h-5" /> New mission

Sign in to plan a mission.

<.live_table fields={fields()} filters={filters()} options={@options} streams={@streams} actions={actions_for(@current_scope)} />
""" end end