feat: port basic functionality to elixir

This commit is contained in:
Schuwi
2025-09-14 12:19:44 +02:00
parent 0a6b7e08e2
commit 5e49cb79a0
14 changed files with 1405 additions and 14 deletions

View File

@@ -0,0 +1,44 @@
defmodule ComponentsElixir.Inventory.Category do
@moduledoc """
Schema for component categories.
Categories can be hierarchical with parent-child relationships.
"""
use Ecto.Schema
import Ecto.Changeset
alias ComponentsElixir.Inventory.{Category, Component}
schema "categories" do
field :name, :string
field :description, :string
belongs_to :parent, Category
has_many :children, Category, foreign_key: :parent_id
has_many :components, Component
timestamps()
end
@doc false
def changeset(category, attrs) do
category
|> cast(attrs, [:name, :description, :parent_id])
|> validate_required([:name])
|> validate_length(:name, min: 1, max: 255)
|> validate_length(:description, max: 1000)
|> unique_constraint([:name, :parent_id])
|> foreign_key_constraint(:parent_id)
end
@doc """
Returns the full path of the category including parent names.
"""
def full_path(%Category{parent: nil} = category), do: category.name
def full_path(%Category{parent: %Category{} = parent} = category) do
"#{full_path(parent)} > #{category.name}"
end
def full_path(%Category{parent: %Ecto.Association.NotLoaded{}} = category) do
category.name
end
end

View File

@@ -0,0 +1,86 @@
defmodule ComponentsElixir.Inventory.Component do
@moduledoc """
Schema for electronic components.
Each component belongs to a category and tracks inventory information
including count, location, and optional image and datasheet.
"""
use Ecto.Schema
import Ecto.Changeset
alias ComponentsElixir.Inventory.Category
schema "components" do
field :name, :string
field :description, :string
field :keywords, :string
field :position, :string
field :count, :integer, default: 0
field :datasheet_url, :string
field :image_filename, :string
belongs_to :category, Category
timestamps()
end
@doc false
def changeset(component, attrs) do
component
|> cast(attrs, [:name, :description, :keywords, :position, :count, :datasheet_url, :image_filename, :category_id])
|> validate_required([:name, :category_id])
|> validate_length(:name, min: 1, max: 255)
|> validate_length(:description, max: 2000)
|> validate_length(:keywords, max: 500)
|> validate_length(:position, max: 100)
|> validate_number(:count, greater_than_or_equal_to: 0)
|> validate_url(:datasheet_url)
|> foreign_key_constraint(:category_id)
end
@doc """
Changeset for updating component count.
"""
def count_changeset(component, attrs) do
component
|> cast(attrs, [:count])
|> validate_required([:count])
|> validate_number(:count, greater_than_or_equal_to: 0)
end
@doc """
Changeset for updating component image.
"""
def image_changeset(component, attrs) do
component
|> cast(attrs, [:image_filename])
end
defp validate_url(changeset, field) do
validate_change(changeset, field, fn ^field, url ->
if url && url != "" do
case URI.parse(url) do
%URI{scheme: scheme} when scheme in ["http", "https"] -> []
_ -> [{field, "must be a valid URL"}]
end
else
[]
end
end)
end
@doc """
Returns true if the component has an image.
"""
def has_image?(%__MODULE__{image_filename: filename}) when is_binary(filename), do: true
def has_image?(_), do: false
@doc """
Returns the search text for this component.
"""
def search_text(%__MODULE__{} = component) do
[component.name, component.description, component.keywords]
|> Enum.filter(&is_binary/1)
|> Enum.join(" ")
end
end