diff --git a/lib/components_elixir/inventory.ex b/lib/components_elixir/inventory.ex index 795cc45..2bcb378 100644 --- a/lib/components_elixir/inventory.ex +++ b/lib/components_elixir/inventory.ex @@ -266,7 +266,7 @@ defmodule ComponentsElixir.Inventory do def list_components(opts \\ []) do Component |> apply_component_filters(opts) - |> order_by([c], [asc: c.name]) + |> apply_component_sorting(opts) |> preload([:category, :storage_location]) |> Repo.all() end @@ -292,6 +292,20 @@ defmodule ComponentsElixir.Inventory do end) end + defp apply_component_sorting(query, opts) do + case Keyword.get(opts, :sort_criteria, "name_asc") do + "name_asc" -> order_by(query, [c], [asc: c.name]) + "name_desc" -> order_by(query, [c], [desc: c.name]) + "inserted_at_asc" -> order_by(query, [c], [asc: c.inserted_at]) + "inserted_at_desc" -> order_by(query, [c], [desc: c.inserted_at]) + "updated_at_asc" -> order_by(query, [c], [asc: c.updated_at]) + "updated_at_desc" -> order_by(query, [c], [desc: c.updated_at]) + "count_asc" -> order_by(query, [c], [asc: c.count]) + "count_desc" -> order_by(query, [c], [desc: c.count]) + _ -> order_by(query, [c], [asc: c.name]) # Default fallback + end + end + @doc """ Gets a single component. """ diff --git a/lib/components_elixir_web/live/components_live.ex b/lib/components_elixir_web/live/components_live.ex index 533bae6..db2ad05 100644 --- a/lib/components_elixir_web/live/components_live.ex +++ b/lib/components_elixir_web/live/components_live.ex @@ -23,12 +23,16 @@ defmodule ComponentsElixirWeb.ComponentsLive do |> assign(:storage_locations, storage_locations) |> assign(:stats, stats) |> assign(:search, "") - |> assign(:sort_criteria, "all_not_id") + |> assign(:sort_criteria, "name_asc") |> assign(:selected_category, nil) |> assign(:offset, 0) |> assign(:components, []) |> assign(:has_more, false) |> assign(:loading, false) + |> assign(:sort_freeze_until, nil) + |> assign(:interacting_with, nil) + |> assign(:sort_freeze_timer, nil) + |> assign(:sort_frozen, false) |> assign(:show_add_form, false) |> assign(:show_edit_form, false) |> assign(:editing_component, nil) @@ -48,7 +52,7 @@ defmodule ComponentsElixirWeb.ComponentsLive do @impl true def handle_params(params, _uri, socket) do search = Map.get(params, "search", "") - criteria = Map.get(params, "criteria", "all_not_id") + criteria = Map.get(params, "criteria", "name_asc") {:noreply, socket @@ -114,10 +118,36 @@ defmodule ComponentsElixirWeb.ComponentsLive do case Inventory.increment_component_count(component) do {:ok, _updated_component} -> - {:noreply, - socket - |> put_flash(:info, "Count updated") - |> load_components()} + # Only apply sort freeze for dynamic sorting criteria + should_freeze = socket.assigns.sort_criteria in ["count_asc", "count_desc", "updated_at_asc", "updated_at_desc"] + + if should_freeze do + # Cancel any existing timer + if socket.assigns.sort_freeze_timer do + Process.cancel_timer(socket.assigns.sort_freeze_timer) + end + + # Set sort freeze for 3 seconds and mark component as interacting + freeze_until = DateTime.add(DateTime.utc_now(), 3, :second) + + # Set new timer to clear interaction state + timer_ref = Process.send_after(self(), {:clear_interaction, id}, 3000) + + {:noreply, + socket + |> put_flash(:info, "Count updated") + |> assign(:sort_freeze_until, freeze_until) + |> assign(:interacting_with, id) + |> assign(:sort_freeze_timer, timer_ref) + |> assign(:sort_frozen, true) + |> load_components()} + else + # Normal behavior for stable sorts + {:noreply, + socket + |> put_flash(:info, "Count updated") + |> load_components()} + end {:error, _changeset} -> {:noreply, put_flash(socket, :error, "Failed to update count")} @@ -129,10 +159,36 @@ defmodule ComponentsElixirWeb.ComponentsLive do case Inventory.decrement_component_count(component) do {:ok, _updated_component} -> - {:noreply, - socket - |> put_flash(:info, "Count updated") - |> load_components()} + # Only apply sort freeze for dynamic sorting criteria + should_freeze = socket.assigns.sort_criteria in ["count_asc", "count_desc", "updated_at_asc", "updated_at_desc"] + + if should_freeze do + # Cancel any existing timer + if socket.assigns.sort_freeze_timer do + Process.cancel_timer(socket.assigns.sort_freeze_timer) + end + + # Set sort freeze for 3 seconds and mark component as interacting + freeze_until = DateTime.add(DateTime.utc_now(), 3, :second) + + # Set new timer to clear interaction state + timer_ref = Process.send_after(self(), {:clear_interaction, id}, 3000) + + {:noreply, + socket + |> put_flash(:info, "Count updated") + |> assign(:sort_freeze_until, freeze_until) + |> assign(:interacting_with, id) + |> assign(:sort_freeze_timer, timer_ref) + |> assign(:sort_frozen, true) + |> load_components()} + else + # Normal behavior for stable sorts + {:noreply, + socket + |> put_flash(:info, "Count updated") + |> load_components()} + end {:error, _changeset} -> {:noreply, put_flash(socket, :error, "Failed to update count")} @@ -270,35 +326,78 @@ defmodule ComponentsElixirWeb.ComponentsLive do end end + @impl true + def handle_info({:clear_interaction, component_id}, socket) do + # Only clear if this timer is for the currently interacting component + if socket.assigns.interacting_with == component_id do + # Clear interaction state and allow sorting to resume + {:noreply, + socket + |> assign(:sort_freeze_until, nil) + |> assign(:interacting_with, nil) + |> assign(:sort_freeze_timer, nil) + |> assign(:sort_frozen, false) + |> load_components()} + else + # Ignore stale timer messages + {:noreply, socket} + end + end + defp load_components(socket, opts \\ []) do append = Keyword.get(opts, :append, false) - filters = [ - search: socket.assigns.search, - sort_criteria: socket.assigns.sort_criteria, - category_id: socket.assigns.selected_category, - limit: @items_per_page, - offset: socket.assigns.offset - ] - |> Enum.reject(fn - {_, v} when is_nil(v) -> true - {:search, v} when v == "" -> true - {_, _} -> false - end) + # Check if sorting should be frozen + now = DateTime.utc_now() + should_reload = is_nil(socket.assigns.sort_freeze_until) || + DateTime.compare(now, socket.assigns.sort_freeze_until) != :lt - %{components: new_components, has_more: has_more} = - Inventory.paginate_components(filters) + if should_reload do + # Normal loading - query database with current sort criteria + filters = [ + search: socket.assigns.search, + sort_criteria: socket.assigns.sort_criteria, + category_id: socket.assigns.selected_category, + limit: @items_per_page, + offset: socket.assigns.offset + ] + |> Enum.reject(fn + {_, v} when is_nil(v) -> true + {:search, v} when v == "" -> true + {_, _} -> false + end) - components = - if append do - socket.assigns.components ++ new_components + %{components: new_components, has_more: has_more} = + Inventory.paginate_components(filters) + + components = + if append do + socket.assigns.components ++ new_components + else + new_components + end + + socket + |> assign(:components, components) + |> assign(:has_more, has_more) + else + # Frozen - just update the specific component in place without reordering + if socket.assigns.interacting_with do + updated_components = + Enum.map(socket.assigns.components, fn component -> + if to_string(component.id) == socket.assigns.interacting_with do + # Reload this specific component to get updated count + Inventory.get_component!(component.id) + else + component + end + end) + + assign(socket, :components, updated_components) else - new_components + socket end - - socket - |> assign(:components, components) - |> assign(:has_more, has_more) + end end defp build_query_params(socket, overrides) do @@ -421,24 +520,44 @@ defmodule ComponentsElixirWeb.ComponentsLive do
-
+ + <%= if @sort_frozen do %> +
+ + + + Sort temporarily frozen +
+ <% end %>
@@ -689,7 +808,11 @@ defmodule ComponentsElixirWeb.ComponentsLive do