diff --git a/lib/components_elixir/inventory.ex b/lib/components_elixir/inventory.ex index 905add6..2083847 100644 --- a/lib/components_elixir/inventory.ex +++ b/lib/components_elixir/inventory.ex @@ -203,6 +203,27 @@ defmodule ComponentsElixir.Inventory do Category.changeset(category, attrs) end + @doc """ + Gets all category IDs that are descendants of the given category ID, including the category itself. + This is used for filtering components by category and all its subcategories. + Returns an empty list if the category doesn't exist. + + Note: This implementation loads all categories into memory for traversal, which is efficient + for typical category tree sizes (hundreds of categories). For very large category trees, + a recursive CTE query could be used instead. + """ + def get_category_and_descendant_ids(category_id) when is_integer(category_id) do + categories = list_categories() + + # Verify the category exists before getting descendants + case Enum.find(categories, &(&1.id == category_id)) do + nil -> [] + _category -> ComponentsElixir.Inventory.Hierarchical.descendant_ids(categories, category_id, &(&1.parent_id)) + end + end + + def get_category_and_descendant_ids(_), do: [] + ## Components @doc """ @@ -219,7 +240,9 @@ defmodule ComponentsElixir.Inventory do defp apply_component_filters(query, opts) do Enum.reduce(opts, query, fn {:category_id, category_id}, query when not is_nil(category_id) -> - where(query, [c], c.category_id == ^category_id) + # Get the category and all its descendant category IDs + category_ids = get_category_and_descendant_ids(category_id) + 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) diff --git a/lib/components_elixir/inventory/hierarchical.ex b/lib/components_elixir/inventory/hierarchical.ex index d938953..02153cb 100644 --- a/lib/components_elixir/inventory/hierarchical.ex +++ b/lib/components_elixir/inventory/hierarchical.ex @@ -143,6 +143,32 @@ defmodule ComponentsElixir.Inventory.Hierarchical do end) end + @doc """ + Gets all descendant IDs for a given entity ID, including the entity itself. + This recursively finds all children, grandchildren, etc. + + ## Examples + iex> categories = [ + ...> %{id: 1, parent_id: nil}, + ...> %{id: 2, parent_id: 1}, + ...> %{id: 3, parent_id: 2}, + ...> %{id: 4, parent_id: 1} + ...> ] + iex> Hierarchical.descendant_ids(categories, 1, &(&1.parent_id)) + [1, 2, 3, 4] + """ + def descendant_ids(entities, entity_id, parent_id_accessor_fn) do + [entity_id | get_descendant_ids_recursive(entities, entity_id, parent_id_accessor_fn)] + end + + defp get_descendant_ids_recursive(entities, parent_id, parent_id_accessor_fn) do + children = child_entities(entities, parent_id, parent_id_accessor_fn) + + Enum.flat_map(children, fn child -> + [child.id | get_descendant_ids_recursive(entities, child.id, parent_id_accessor_fn)] + end) + end + @doc """ Generates display name for entity including parent context. For dropdown displays: "Parent > Child"