diff --git a/lib/microwaveprop_web/live/rover_locations_live.ex b/lib/microwaveprop_web/live/rover_locations_live.ex index 6b57dea3..45c6f139 100644 --- a/lib/microwaveprop_web/live/rover_locations_live.ex +++ b/lib/microwaveprop_web/live/rover_locations_live.ex @@ -1,38 +1,81 @@ defmodule MicrowavepropWeb.RoverLocationsLive do @moduledoc """ Globally-shared list of rover-friendly (or off-limits) parking - locations. Everyone sees the list; only logged-in users can add or + locations. Everyone sees the table; only logged-in users can add or edit entries (and only the creator can edit/delete their own). """ use MicrowavepropWeb, :live_view + use MicrowavepropWeb.LiveTableResource, schema: Microwaveprop.Rover.Location + + import Ecto.Query alias Microwaveprop.Accounts.User alias Microwaveprop.Radio.Maidenhead + alias Microwaveprop.Repo alias Microwaveprop.Rover alias Microwaveprop.Rover.Location + @valid_status_filters ~w(ideal off_limits) + + def table_options do + %{sorting: %{default_sort: [inserted_at: :desc]}} + end + + def fields do + [ + id: %{label: "ID", hidden: true}, + user_id: %{label: "Owner", hidden: true}, + status: %{label: "Status", sortable: true, renderer: &status_cell/1}, + lat: %{label: "Location", sortable: true, renderer: &location_cell/2}, + lon: %{label: "Lon", hidden: true}, + notes: %{label: "Notes", searchable: true, renderer: ¬es_cell/1}, + inserted_at: %{label: "Added (UTC)", sortable: true, renderer: &format_ts/1} + ] + end + + def filters, do: [] + + @doc false + # Called by LiveTable to get the base queryable. Filters by status when + # the user picks one in the in-page select; nil means show all. + def visible_query_provider(status_filter) do + base = from(l in Location, as: :resource) + + case status_filter do + "ideal" -> from(l in base, where: l.status == :ideal) + "off_limits" -> from(l in base, where: l.status == :off_limits) + _ -> base + end + end + @impl true def mount(_params, _session, socket) do {:ok, - assign(socket, + socket + |> assign( page_title: "Rover Locations", - locations: Rover.list_locations(), form: nil, editing_id: nil, - expanded_id: nil - )} + status_filter: nil + ) + |> assign(:data_provider, {__MODULE__, :visible_query_provider, [nil]})} end @impl true - def handle_event("toggle_map", %{"id" => id}, socket) do - new_id = if socket.assigns.expanded_id == id, do: nil, else: id - {:noreply, assign(socket, expanded_id: new_id)} + def handle_event("filter_status", %{"value" => value}, socket) do + status = parse_status(value) + data_provider = {__MODULE__, :visible_query_provider, [status]} + + {:noreply, + socket + |> assign(:status_filter, status) + |> assign(:data_provider, data_provider) + |> reload()} end def handle_event("new", _params, socket) do if user = current_user(socket) do - form = blank_form(user) - {:noreply, assign(socket, form: form, editing_id: nil)} + {:noreply, assign(socket, form: blank_form(user), editing_id: nil)} else {:noreply, put_flash(socket, :error, "Sign in to add a location.")} end @@ -41,7 +84,7 @@ defmodule MicrowavepropWeb.RoverLocationsLive do def handle_event("edit", %{"id" => id}, socket) do case current_user(socket) do %User{id: user_id} -> - case Enum.find(socket.assigns.locations, &(&1.id == id)) do + case Repo.get(Location, id) do %Location{user_id: ^user_id} = loc -> cs = Location.changeset(loc, %{}) {:noreply, assign(socket, form: to_form(cs), editing_id: id)} @@ -87,8 +130,8 @@ defmodule MicrowavepropWeb.RoverLocationsLive do {:ok, _} -> {:noreply, socket - |> assign(locations: Rover.list_locations()) - |> put_flash(:info, "Location removed.")} + |> put_flash(:info, "Location removed.") + |> reload()} {:error, :not_found} -> {:noreply, put_flash(socket, :error, "You can only delete your own locations.")} @@ -104,8 +147,9 @@ defmodule MicrowavepropWeb.RoverLocationsLive do {:ok, _loc} -> {:noreply, socket - |> assign(form: nil, editing_id: nil, locations: Rover.list_locations()) - |> put_flash(:info, "Location added.")} + |> assign(form: nil, editing_id: nil) + |> put_flash(:info, "Location added.") + |> reload()} {:error, cs} -> {:noreply, assign(socket, form: to_form(cs))} @@ -117,8 +161,9 @@ defmodule MicrowavepropWeb.RoverLocationsLive do {:ok, _loc} -> {:noreply, socket - |> assign(form: nil, editing_id: nil, locations: Rover.list_locations()) - |> put_flash(:info, "Location updated.")} + |> assign(form: nil, editing_id: nil) + |> put_flash(:info, "Location updated.") + |> reload()} {:error, :not_found} -> {:noreply, put_flash(socket, :error, "You can only edit your own locations.")} @@ -128,14 +173,34 @@ defmodule MicrowavepropWeb.RoverLocationsLive do end end - defp location_for_edit(socket) do - Enum.find(socket.assigns.locations, &(&1.id == socket.assigns.editing_id)) || %Location{} + defp location_for_edit(%{assigns: %{editing_id: id}}) when is_binary(id) do + Repo.get(Location, id) || %Location{} end + defp location_for_edit(_), do: %Location{} + defp blank_form(_user) do %Location{} |> Location.changeset(%{}) |> to_form() end + # Re-runs the LiveTable fetch with the current data_provider + options. + # Cheaper and simpler than push_patch — preserves search/sort/page state + # without round-tripping through the URL. + 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 parse_status(value) when value in @valid_status_filters, do: value + defp parse_status(_), do: nil + defp current_user(%Phoenix.LiveView.Socket{assigns: assigns}), do: scope_user(assigns) defp current_user(assigns) when is_map(assigns), do: scope_user(assigns) @@ -146,157 +211,180 @@ defmodule MicrowavepropWeb.RoverLocationsLive do end end - defp can_edit?(_assigns, %Location{user_id: nil}), do: false + defp owner?(_scope, %{user_id: nil}), do: false - defp can_edit?(assigns, %Location{user_id: uid}) do - case current_user(assigns) do + defp owner?(scope, %{user_id: uid}) do + case scope_user(%{current_scope: scope}) do %User{id: ^uid} -> true _ -> false end end - defp status_label(:ideal), do: "Ideal" - defp status_label(:off_limits), do: "Off Limits" + defp owner?(_, _), do: false - defp status_class(:ideal), do: "badge badge-success" - defp status_class(:off_limits), do: "badge badge-error" + defp actions_for(scope) do + [ + buttons: fn %{record: record} -> + row_actions(record, scope) + end + ] + end + + defp row_actions(record, scope) do + assigns = %{record: record, owner?: owner?(scope, record)} + + ~H""" +
- Community-shared parking spots for portable rover ops. -
-- Sign in to add or edit locations. -
++ Sign in to add or edit locations. +
-- {loc.notes} -
-- Added {Calendar.strftime(loc.inserted_at, "%Y-%m-%d")} -
-