Files
component-system/lib/components_elixir/inventory/storage_location.ex
2025-09-20 11:52:43 +02:00

103 lines
2.8 KiB
Elixir

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(type: :naive_datetime_usec)
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