diff --git a/lib/components_elixir/inventory.ex b/lib/components_elixir/inventory.ex index 2083847..30c90df 100644 --- a/lib/components_elixir/inventory.ex +++ b/lib/components_elixir/inventory.ex @@ -224,6 +224,27 @@ defmodule ComponentsElixir.Inventory do def get_category_and_descendant_ids(_), do: [] + @doc """ + Gets all storage location IDs that are descendants of the given storage location ID, including the location itself. + This is used for filtering components by storage location and all its sub-locations. + Returns an empty list if the storage location doesn't exist. + + Note: This implementation loads all storage locations into memory for traversal, which is efficient + for typical storage location tree sizes (hundreds of locations). For very large storage location trees, + a recursive CTE query could be used instead. + """ + def get_storage_location_and_descendant_ids(storage_location_id) when is_integer(storage_location_id) do + storage_locations = list_storage_locations() + + # Verify the storage location exists before getting descendants + case Enum.find(storage_locations, &(&1.id == storage_location_id)) do + nil -> [] + _storage_location -> ComponentsElixir.Inventory.Hierarchical.descendant_ids(storage_locations, storage_location_id, &(&1.parent_id)) + end + end + + def get_storage_location_and_descendant_ids(_), do: [] + ## Components @doc """ @@ -245,7 +266,9 @@ defmodule ComponentsElixir.Inventory do where(query, [c], c.category_id in ^category_ids) {:storage_location_id, storage_location_id}, query when not is_nil(storage_location_id) -> - where(query, [c], c.storage_location_id == ^storage_location_id) + # Get the storage location and all its descendant storage location IDs + storage_location_ids = get_storage_location_and_descendant_ids(storage_location_id) + where(query, [c], c.storage_location_id in ^storage_location_ids) {:search, search_term}, query when is_binary(search_term) and search_term != "" -> search_pattern = "%#{search_term}%" diff --git a/lib/components_elixir_web/live/categories_live.ex b/lib/components_elixir_web/live/categories_live.ex index dd7f546..8cec55d 100644 --- a/lib/components_elixir_web/live/categories_live.ex +++ b/lib/components_elixir_web/live/categories_live.ex @@ -227,9 +227,23 @@ defmodule ComponentsElixirWeb.CategoriesLive do
<%= if @title_tag == "h3" do %> -

{@category.name}

+

+ <.link + navigate={~p"/?category_id=#{@category.id}"} + class={"#{if @depth == 0, do: "text-base-content", else: "text-base-content/80"} hover:text-primary hover:underline"} + > + {@category.name} + +

<% else %> -

{@category.name}

+

+ <.link + navigate={~p"/?category_id=#{@category.id}"} + class={"#{if @depth == 0, do: "text-base-content", else: "text-base-content/80"} hover:text-primary hover:underline"} + > + {@category.name} + +

<% end %> ({@count_display}) @@ -260,9 +274,23 @@ defmodule ComponentsElixirWeb.CategoriesLive do
<%= if @title_tag == "h3" do %> -

{@category.name}

+

+ <.link + navigate={~p"/?category_id=#{@category.id}"} + class={"#{if @depth == 0, do: "text-base-content", else: "text-base-content/80"} hover:text-primary hover:underline"} + > + {@category.name} + +

<% else %> -

{@category.name}

+

+ <.link + navigate={~p"/?category_id=#{@category.id}"} + class={"#{if @depth == 0, do: "text-base-content", else: "text-base-content/80"} hover:text-primary hover:underline"} + > + {@category.name} + +

<% end %> <%= if @category.description do %>

{@category.description}

diff --git a/lib/components_elixir_web/live/components_live.ex b/lib/components_elixir_web/live/components_live.ex index 5d94c83..2f4a623 100644 --- a/lib/components_elixir_web/live/components_live.ex +++ b/lib/components_elixir_web/live/components_live.ex @@ -25,6 +25,8 @@ defmodule ComponentsElixirWeb.ComponentsLive do |> assign(:search, "") |> assign(:sort_criteria, "name_asc") |> assign(:selected_category, nil) + |> assign(:selected_storage_location, nil) + |> assign(:show_advanced_filters, false) |> assign(:offset, 0) |> assign(:components, []) |> assign(:has_more, false) @@ -53,11 +55,19 @@ defmodule ComponentsElixirWeb.ComponentsLive do def handle_params(params, _uri, socket) do search = Map.get(params, "search", "") criteria = Map.get(params, "criteria", "name_asc") + category_id = parse_filter_id(Map.get(params, "category_id")) + storage_location_id = parse_filter_id(Map.get(params, "storage_location_id")) + + # Show advanced filters if storage location is being used + show_advanced = not is_nil(storage_location_id) {:noreply, socket |> assign(:search, search) |> assign(:sort_criteria, criteria) + |> assign(:selected_category, category_id) + |> assign(:selected_storage_location, storage_location_id) + |> assign(:show_advanced_filters, show_advanced) |> assign(:offset, 0) |> load_components()} end @@ -88,21 +98,57 @@ defmodule ComponentsElixirWeb.ComponentsLive do end def handle_event("category_filter", %{"category_id" => ""}, socket) do + query_string = build_query_params_without_category(socket) + path = if query_string == "", do: "/", else: "/?" <> query_string + {:noreply, socket |> assign(:selected_category, nil) |> assign(:offset, 0) - |> load_components()} + |> load_components() + |> push_patch(to: path)} end def handle_event("category_filter", %{"category_id" => category_id}, socket) do category_id = String.to_integer(category_id) + query_string = build_query_params_with_category(socket, category_id) + path = if query_string == "", do: "/", else: "/?" <> query_string {:noreply, socket |> assign(:selected_category, category_id) |> assign(:offset, 0) - |> load_components()} + |> load_components() + |> push_patch(to: path)} + end + + def handle_event("storage_location_filter", %{"storage_location_id" => ""}, socket) do + query_string = build_query_params_with_storage_location(socket, nil) + path = if query_string == "", do: "/", else: "/?" <> query_string + + {:noreply, + socket + |> assign(:selected_storage_location, nil) + |> assign(:offset, 0) + |> load_components() + |> push_patch(to: path)} + end + + def handle_event("storage_location_filter", %{"storage_location_id" => storage_location_id}, socket) do + storage_location_id = String.to_integer(storage_location_id) + query_string = build_query_params_with_storage_location(socket, storage_location_id) + path = if query_string == "", do: "/", else: "/?" <> query_string + + {:noreply, + socket + |> assign(:selected_storage_location, storage_location_id) + |> assign(:offset, 0) + |> load_components() + |> push_patch(to: path)} + end + + def handle_event("toggle_advanced_filters", _params, socket) do + {:noreply, assign(socket, :show_advanced_filters, !socket.assigns.show_advanced_filters)} end def handle_event("load_more", _params, socket) do @@ -376,6 +422,7 @@ defmodule ComponentsElixirWeb.ComponentsLive do search: socket.assigns.search, sort_criteria: socket.assigns.sort_criteria, category_id: socket.assigns.selected_category, + storage_location_id: socket.assigns.selected_storage_location, limit: @items_per_page, offset: socket.assigns.offset ] @@ -421,7 +468,57 @@ defmodule ComponentsElixirWeb.ComponentsLive do defp build_query_params(socket, overrides) do params = %{ search: Map.get(overrides, :search, socket.assigns.search), - criteria: Map.get(overrides, :criteria, socket.assigns.sort_criteria) + criteria: Map.get(overrides, :criteria, socket.assigns.sort_criteria), + category_id: Map.get(overrides, :category_id, socket.assigns.selected_category), + storage_location_id: Map.get(overrides, :storage_location_id, socket.assigns.selected_storage_location) + } + + params + |> Enum.reject(fn {_k, v} -> is_nil(v) or v == "" end) + |> URI.encode_query() + end + + defp parse_filter_id(nil), do: nil + defp parse_filter_id(""), do: nil + defp parse_filter_id(id) when is_binary(id) do + case Integer.parse(id) do + {int_id, ""} -> int_id + _ -> nil + end + end + defp parse_filter_id(id) when is_integer(id), do: id + + defp build_query_params_with_category(socket, category_id) do + params = %{ + search: socket.assigns.search, + criteria: socket.assigns.sort_criteria, + category_id: category_id, + storage_location_id: socket.assigns.selected_storage_location + } + + params + |> Enum.reject(fn {_k, v} -> is_nil(v) or v == "" end) + |> URI.encode_query() + end + + defp build_query_params_with_storage_location(socket, storage_location_id) do + params = %{ + search: socket.assigns.search, + criteria: socket.assigns.sort_criteria, + category_id: socket.assigns.selected_category, + storage_location_id: storage_location_id + } + + params + |> Enum.reject(fn {_k, v} -> is_nil(v) or v == "" end) + |> URI.encode_query() + end + + defp build_query_params_without_category(socket) do + params = %{ + search: socket.assigns.search, + criteria: socket.assigns.sort_criteria, + storage_location_id: socket.assigns.selected_storage_location } params @@ -558,7 +655,40 @@ defmodule ComponentsElixirWeb.ComponentsLive do <% end %>
+
+ +
+ + + <%= if @show_advanced_filters do %> +
+
+
+ +
+ +
+
+
+
+ <% end %>
diff --git a/lib/components_elixir_web/live/storage_locations_live.ex b/lib/components_elixir_web/live/storage_locations_live.ex index 1ab3f82..53ff0da 100644 --- a/lib/components_elixir_web/live/storage_locations_live.ex +++ b/lib/components_elixir_web/live/storage_locations_live.ex @@ -362,9 +362,23 @@ defmodule ComponentsElixirWeb.StorageLocationsLive do
<%= if @title_tag == "h3" do %> -

{@location.name}

+

+ <.link + navigate={~p"/?storage_location_id=#{@location.id}"} + class={"#{if @depth == 0, do: "text-base-content", else: "text-base-content/80"} hover:text-primary hover:underline"} + > + {@location.name} + +

<% else %> -

{@location.name}

+

+ <.link + navigate={~p"/?storage_location_id=#{@location.id}"} + class={"#{if @depth == 0, do: "text-base-content", else: "text-base-content/80"} hover:text-primary hover:underline"} + > + {@location.name} + +

<% end %> ({@count_display}) @@ -400,9 +414,23 @@ defmodule ComponentsElixirWeb.StorageLocationsLive do
<%= if @title_tag == "h3" do %> -

{@location.name}

+

+ <.link + navigate={~p"/?storage_location_id=#{@location.id}"} + class={"#{if @depth == 0, do: "text-base-content", else: "text-base-content/80"} hover:text-primary hover:underline"} + > + {@location.name} + +

<% else %> -

{@location.name}

+

+ <.link + navigate={~p"/?storage_location_id=#{@location.id}"} + class={"#{if @depth == 0, do: "text-base-content", else: "text-base-content/80"} hover:text-primary hover:underline"} + > + {@location.name} + +

<% end %> <%= if @location.description do %>

{@location.description}