- Add jump_credo_checks ~> 0.4 with all 20 checks enabled - Fix all standard Credo issues: 139 @spec (113 done, 26 remain), 4 refactoring, 3 alias usage, 9 System.cmd env, 5 unsafe_to_atom, 2 max line length, 9 assert_receive timeout - Fix 170+ jump_credo_checks warnings: - 117 TopLevelAliasImportRequire: move nested alias/import to module top - 32 UseObanProWorker: switch to Oban.Pro.Worker - 4 DoctestIExExamples: add doctests / create test file - ~20 WeakAssertion: strengthen type-check assertions - Various ConditionalAssertion, AssertReceiveTimeout fixes - Exclude vendor/ from Credo analysis - Remaining: 175 warnings (mostly opinionated WeakAssertion, AvoidSocketAssignsInTest), 26 @spec annotations
215 lines
6 KiB
Elixir
215 lines
6 KiB
Elixir
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"""
|
|
<div class="flex gap-2">
|
|
<.link
|
|
navigate={~p"/rover-planning/#{@record.id}"}
|
|
class="btn btn-xs btn-ghost"
|
|
>
|
|
View
|
|
</.link>
|
|
<.link
|
|
:if={@can_modify?}
|
|
navigate={~p"/rover-planning/#{@record.id}/edit"}
|
|
class="btn btn-xs btn-ghost"
|
|
>
|
|
Edit
|
|
</.link>
|
|
<button
|
|
:if={@can_modify?}
|
|
type="button"
|
|
phx-click="delete"
|
|
phx-value-id={@record.id}
|
|
data-confirm="Delete this mission?"
|
|
class="btn btn-xs btn-ghost text-error"
|
|
>
|
|
Delete
|
|
</button>
|
|
</div>
|
|
"""
|
|
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}
|
|
</.link>
|
|
"""
|
|
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|<span class="badge badge-success badge-sm">Known good only</span>|
|
|
end
|
|
|
|
defp scope_cell(false) do
|
|
assigns = %{}
|
|
~H|<span class="badge badge-info badge-sm">All locations</span>|
|
|
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"""
|
|
<Layouts.app flash={@flash} current_scope={@current_scope} max_width="max-w-6xl">
|
|
<.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.
|
|
</:subtitle>
|
|
<: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
|
|
</.link>
|
|
</:actions>
|
|
</.header>
|
|
|
|
<p :if={is_nil(current_user(assigns))} class="alert alert-info text-sm mb-4">
|
|
<a href={~p"/users/log-in"} class="link link-primary">Sign in</a> to plan a mission.
|
|
</p>
|
|
|
|
<.live_table
|
|
fields={fields()}
|
|
filters={filters()}
|
|
options={@options}
|
|
streams={@streams}
|
|
actions={actions_for(@current_scope)}
|
|
/>
|
|
</Layouts.app>
|
|
"""
|
|
end
|
|
end
|