defmodule ComponentsElixir.Inventory.StorageLocation do @moduledoc """ Schema for storage locations with hierarchical organization. Storage locations can be nested (shelf -> drawer -> box) and each has a unique AprilTag for quick scanning and identification. """ use Ecto.Schema use ComponentsElixir.Inventory.HierarchicalSchema import Ecto.Changeset import Ecto.Query alias ComponentsElixir.Inventory.{StorageLocation, Component} schema "storage_locations" do field :name, :string field :description, :string field :apriltag_id, :integer # Only parent relationship is stored belongs_to :parent, StorageLocation has_many :children, StorageLocation, foreign_key: :parent_id has_many :components, Component timestamps() end @doc false def changeset(storage_location, attrs) do storage_location |> cast(attrs, [:name, :description, :parent_id, :apriltag_id]) |> validate_required([:name]) |> validate_length(:name, min: 1, max: 100) |> validate_length(:description, max: 500) |> validate_apriltag_id() |> foreign_key_constraint(:parent_id) |> put_apriltag_id() end # HierarchicalSchema implementations @impl true def full_path(%StorageLocation{} = storage_location) do Hierarchical.full_path(storage_location, &(&1.parent), path_separator()) end @impl true def parent(%StorageLocation{parent: parent}), do: parent @impl true def children(%StorageLocation{children: children}), do: children @impl true def path_separator(), do: " / " @impl true def entity_type(), do: :storage_location @doc """ Returns the AprilTag format for this storage location. Returns the AprilTag ID that corresponds to this location. """ def apriltag_format(storage_location) do storage_location.apriltag_id end # Private functions for changeset processing defp validate_apriltag_id(changeset) do changeset |> validate_number(:apriltag_id, greater_than_or_equal_to: 0, less_than_or_equal_to: 586) |> unique_constraint(:apriltag_id, message: "AprilTag ID is already in use") end defp put_apriltag_id(changeset) do case get_field(changeset, :apriltag_id) do nil -> put_change(changeset, :apriltag_id, get_next_available_apriltag_id()) _ -> changeset end end defp get_next_available_apriltag_id do # Get all used AprilTag IDs used_ids = ComponentsElixir.Repo.all( from sl in ComponentsElixir.Inventory.StorageLocation, where: not is_nil(sl.apriltag_id), select: sl.apriltag_id ) # Find the first available ID (0-586) 0..586 |> Enum.find(&(&1 not in used_ids)) |> case do nil -> # All IDs are used - this should be handled at the application level raise "All AprilTag IDs are in use" id -> id end end end