defmodule ComponentsElixir.Inventory do @moduledoc """ The Inventory context for managing components and categories. """ import Ecto.Query, warn: false alias ComponentsElixir.Repo alias ComponentsElixir.Inventory.{Category, Component} ## Categories @doc """ Returns the list of categories. """ def list_categories do Category |> order_by([c], [asc: c.name]) |> preload(:parent) |> Repo.all() end @doc """ Returns the list of root categories (no parent). """ def list_root_categories do Category |> where([c], is_nil(c.parent_id)) |> order_by([c], [asc: c.name]) |> Repo.all() end @doc """ Returns the list of child categories for a given parent. """ def list_child_categories(parent_id) do Category |> where([c], c.parent_id == ^parent_id) |> order_by([c], [asc: c.name]) |> Repo.all() end @doc """ Gets a single category. """ def get_category!(id) do Category |> preload(:parent) |> Repo.get!(id) end @doc """ Creates a category. """ def create_category(attrs \\ %{}) do %Category{} |> Category.changeset(attrs) |> Repo.insert() end @doc """ Updates a category. """ def update_category(%Category{} = category, attrs) do category |> Category.changeset(attrs) |> Repo.update() end @doc """ Deletes a category. """ def delete_category(%Category{} = category) do Repo.delete(category) end @doc """ Returns an `%Ecto.Changeset{}` for tracking category changes. """ def change_category(%Category{} = category, attrs \\ %{}) do Category.changeset(category, attrs) end @doc """ Returns the count of components in a specific category. """ def count_components_in_category(category_id) do Component |> where([c], c.category_id == ^category_id) |> Repo.aggregate(:count, :id) end ## Components @doc """ Returns the list of components with optional filtering and pagination. """ def list_components(opts \\ []) do Component |> apply_component_filters(opts) |> preload(:category) |> order_by([c], [asc: c.category_id, asc: c.name]) |> Repo.all() end @doc """ Returns paginated components with search and filtering. """ def paginate_components(opts \\ []) do limit = Keyword.get(opts, :limit, 20) offset = Keyword.get(opts, :offset, 0) query = Component |> apply_component_filters(opts) |> preload(:category) |> order_by([c], [asc: c.category_id, asc: c.name]) components = query |> limit(^limit) |> offset(^offset) |> Repo.all() total_count = Repo.aggregate(query, :count, :id) %{ components: components, total_count: total_count, has_more: total_count > offset + length(components) } end defp apply_component_filters(query, opts) do Enum.reduce(opts, query, fn {:search, search}, query when is_binary(search) and search != "" -> if String.length(search) > 3 do # Use full-text search for longer queries where(query, [c], fragment("to_tsvector('english', ? || ' ' || coalesce(?, '') || ' ' || coalesce(?, '')) @@ plainto_tsquery(?)", c.name, c.description, c.keywords, ^search)) else # Use ILIKE for shorter queries search_term = "%#{search}%" where(query, [c], ilike(c.name, ^search_term) or ilike(c.description, ^search_term) or ilike(c.keywords, ^search_term)) end {:category_id, category_id}, query when is_integer(category_id) -> where(query, [c], c.category_id == ^category_id) {:sort_criteria, "name"}, query -> order_by(query, [c], [asc: c.name]) {:sort_criteria, "description"}, query -> order_by(query, [c], [asc: c.description]) {:sort_criteria, "id"}, query -> order_by(query, [c], [asc: c.id]) {:sort_criteria, "category_id"}, query -> order_by(query, [c], [asc: c.category_id, asc: c.name]) _, query -> query end) end @doc """ Gets a single component. """ def get_component!(id) do Component |> preload(:category) |> Repo.get!(id) end @doc """ Creates a component. """ def create_component(attrs \\ %{}) do %Component{} |> Component.changeset(attrs) |> Repo.insert() end @doc """ Updates a component. """ def update_component(%Component{} = component, attrs) do component |> Component.changeset(attrs) |> Repo.update() end @doc """ Updates a component's count. """ def update_component_count(%Component{} = component, count) when is_integer(count) do component |> Component.count_changeset(%{count: count}) |> Repo.update() end @doc """ Increments a component's count. """ def increment_component_count(%Component{} = component) do update_component_count(component, component.count + 1) end @doc """ Decrements a component's count (minimum 0). """ def decrement_component_count(%Component{} = component) do new_count = max(0, component.count - 1) update_component_count(component, new_count) end @doc """ Updates a component's image filename. """ def update_component_image(%Component{} = component, image_filename) do component |> Component.image_changeset(%{image_filename: image_filename}) |> Repo.update() end @doc """ Deletes a component. """ def delete_component(%Component{} = component) do Repo.delete(component) end @doc """ Returns an `%Ecto.Changeset{}` for tracking component changes. """ def change_component(%Component{} = component, attrs \\ %{}) do Component.changeset(component, attrs) end @doc """ Returns component statistics. """ def component_stats do total_components = Repo.aggregate(Component, :count, :id) total_stock = Repo.aggregate(Component, :sum, :count) || 0 categories_with_components = Component |> select([c], c.category_id) |> distinct(true) |> Repo.aggregate(:count, :category_id) %{ total_components: total_components, total_stock: total_stock, categories_with_components: categories_with_components } end end