feat(elixir): storage location system

This commit is contained in:
Schuwi
2025-09-14 15:20:25 +02:00
parent b9126c286f
commit 9d090859e8
14 changed files with 1517 additions and 160 deletions

View File

@@ -0,0 +1,29 @@
defmodule ComponentsElixir.Repo.Migrations.CreateStorageLocations do
use Ecto.Migration
def change do
create table(:storage_locations) do
add :name, :string, null: false
add :description, :text
add :qr_code, :string, null: false
add :level, :integer, default: 0
add :path, :text, null: false
add :is_active, :boolean, default: true
add :parent_id, references(:storage_locations, on_delete: :restrict)
timestamps()
end
create unique_index(:storage_locations, [:qr_code])
create index(:storage_locations, [:parent_id])
create index(:storage_locations, [:level])
create unique_index(:storage_locations, [:name, :parent_id])
# Enable trigram extension for path searching
execute "CREATE EXTENSION IF NOT EXISTS pg_trgm", "DROP EXTENSION IF EXISTS pg_trgm"
# GIN index for fast path-based searches
execute "CREATE INDEX storage_locations_path_gin_idx ON storage_locations USING gin(path gin_trgm_ops)",
"DROP INDEX storage_locations_path_gin_idx"
end
end

View File

@@ -0,0 +1,16 @@
defmodule ComponentsElixir.Repo.Migrations.AddStorageLocationToComponents do
use Ecto.Migration
def change do
alter table(:components) do
add :storage_location_id, references(:storage_locations, on_delete: :nilify_all)
add :legacy_position, :string
end
create index(:components, [:storage_location_id])
# Copy existing position data to legacy_position for migration
execute "UPDATE components SET legacy_position = position WHERE position IS NOT NULL",
"UPDATE components SET position = legacy_position WHERE legacy_position IS NOT NULL"
end
end

View File

@@ -0,0 +1,10 @@
defmodule ComponentsElixir.Repo.Migrations.RemoveNotNullConstraintsFromStorageLocations do
use Ecto.Migration
def change do
alter table(:storage_locations) do
modify :level, :integer, null: true
modify :path, :string, null: true
end
end
end

View File

@@ -11,11 +11,12 @@
# and so on) as they will fail if something goes wrong.
alias ComponentsElixir.{Repo, Inventory}
alias ComponentsElixir.Inventory.{Category, Component}
alias ComponentsElixir.Inventory.{Category, Component, StorageLocation}
# Clear existing data
Repo.delete_all(Component)
Repo.delete_all(Category)
Repo.delete_all(StorageLocation)
# Create categories
{:ok, resistors} = Inventory.create_category(%{name: "Resistors", description: "Various types of resistors"})
@@ -48,13 +49,62 @@ Repo.delete_all(Category)
parent_id: capacitors.id
})
# Create storage locations
{:ok, shelf_a} = Inventory.create_storage_location(%{name: "Shelf A", description: "Main electronics shelf"})
{:ok, shelf_b} = Inventory.create_storage_location(%{name: "Shelf B", description: "Components overflow shelf"})
# Create drawers on Shelf A
{:ok, drawer_a1} = Inventory.create_storage_location(%{
name: "Drawer 1",
description: "Resistors and capacitors",
parent_id: shelf_a.id
})
{:ok, drawer_a2} = Inventory.create_storage_location(%{
name: "Drawer 2",
description: "Semiconductors and ICs",
parent_id: shelf_a.id
})
# Create boxes in Drawer A1
{:ok, box_a1_1} = Inventory.create_storage_location(%{
name: "Box 1",
description: "Through-hole resistors",
parent_id: drawer_a1.id
})
{:ok, box_a1_2} = Inventory.create_storage_location(%{
name: "Box 2",
description: "SMD resistors",
parent_id: drawer_a1.id
})
{:ok, box_a1_3} = Inventory.create_storage_location(%{
name: "Box 3",
description: "Ceramic capacitors",
parent_id: drawer_a1.id
})
# Create boxes in Drawer A2
{:ok, box_a2_1} = Inventory.create_storage_location(%{
name: "Box 1",
description: "Microcontrollers",
parent_id: drawer_a2.id
})
{:ok, _box_a2_2} = Inventory.create_storage_location(%{
name: "Box 2",
description: "Transistors and diodes",
parent_id: drawer_a2.id
})
# Create sample components
sample_components = [
%{
name: "1kΩ Resistor (1/4W)",
description: "Carbon film resistor, 5% tolerance",
keywords: "resistor carbon film 1k ohm",
position: "A1-1",
storage_location_id: box_a1_1.id,
count: 150,
category_id: resistors.id
},
@@ -62,7 +112,7 @@ sample_components = [
name: "10kΩ Resistor (1/4W)",
description: "Carbon film resistor, 5% tolerance",
keywords: "resistor carbon film 10k ohm",
position: "A1-2",
storage_location_id: box_a1_1.id,
count: 200,
category_id: resistors.id
},
@@ -70,7 +120,7 @@ sample_components = [
name: "100μF Electrolytic Capacitor",
description: "25V electrolytic capacitor, radial leads",
keywords: "capacitor electrolytic 100uf microfarad",
position: "B2-1",
storage_location_id: box_a1_3.id,
count: 50,
category_id: capacitors.id
},
@@ -78,7 +128,7 @@ sample_components = [
name: "0.1μF Ceramic Capacitor",
description: "50V ceramic disc capacitor",
keywords: "capacitor ceramic 100nf nanofarad disc",
position: "B2-2",
storage_location_id: box_a1_3.id,
count: 300,
category_id: capacitors.id
},
@@ -86,7 +136,7 @@ sample_components = [
name: "ATmega328P-PU",
description: "8-bit AVR microcontroller, DIP-28 package",
keywords: "microcontroller avr atmega328 arduino",
position: "C3-1",
storage_location_id: box_a2_1.id,
count: 10,
datasheet_url: "https://ww1.microchip.com/downloads/en/DeviceDoc/Atmel-7810-Automotive-Microcontrollers-ATmega328P_Datasheet.pdf",
category_id: semiconductors.id
@@ -95,7 +145,7 @@ sample_components = [
name: "LM358 Op-Amp",
description: "Dual operational amplifier, DIP-8 package",
keywords: "opamp operational amplifier lm358 dual",
position: "C3-2",
storage_location_id: box_a2_1.id,
count: 25,
category_id: semiconductors.id
},
@@ -103,7 +153,7 @@ sample_components = [
name: "2N2222 NPN Transistor",
description: "General purpose NPN transistor, TO-92 package",
keywords: "transistor npn 2n2222 to92",
position: "C3-3",
storage_location_id: box_a2_1.id,
count: 40,
category_id: semiconductors.id
},
@@ -111,7 +161,7 @@ sample_components = [
name: "2.54mm Pin Headers",
description: "Male pin headers, 40 pins, break-away",
keywords: "header pins male 2.54mm breakaway",
position: "D4-1",
storage_location_id: drawer_a2.id,
count: 20,
category_id: connectors.id
},
@@ -119,7 +169,7 @@ sample_components = [
name: "JST-XH 2-pin Connector",
description: "2-pin JST-XH connector with housing",
keywords: "jst xh connector 2pin housing",
position: "D4-2",
storage_location_id: drawer_a2.id,
count: 30,
category_id: connectors.id
},
@@ -127,7 +177,7 @@ sample_components = [
name: "470Ω Resistor (1/4W)",
description: "Carbon film resistor, 5% tolerance, commonly used for LEDs",
keywords: "resistor carbon film 470 ohm led current limiting",
position: "A1-3",
storage_location_id: box_a1_1.id,
count: 100,
category_id: resistors.id
}
@@ -137,8 +187,24 @@ Enum.each(sample_components, fn component_attrs ->
{:ok, _component} = Inventory.create_component(component_attrs)
end)
IO.puts("Seeded database with categories and sample components!")
IO.puts("Seeded database with categories, storage locations, and sample components!")
IO.puts("Categories: #{length(Inventory.list_categories())}")
IO.puts("Storage Locations: #{length(Inventory.list_storage_locations())}")
IO.puts("Components: #{length(Inventory.list_components())}")
IO.puts("")
IO.puts("Sample QR codes for testing:")
# Print some sample QR codes for testing
sample_locations = [
Inventory.get_storage_location!(shelf_a.id),
Inventory.get_storage_location!(drawer_a1.id),
Inventory.get_storage_location!(box_a1_1.id),
Inventory.get_storage_location!(box_a2_1.id)
]
Enum.each(sample_locations, fn location ->
qr_data = ComponentsElixir.QRCode.generate_qr_data(location)
IO.puts("#{location.path}: #{qr_data}")
end)
IO.puts("")
IO.puts("Login with password: changeme (or set AUTH_PASSWORD environment variable)")