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,384 @@
# QR Code Storage Location System Design
## Overview
Implement a hierarchical storage location system with QR code generation and scanning capabilities to enable quick component location entry and filtering.
## Database Schema
### 1. Storage Locations Table
```sql
CREATE TABLE storage_locations (
id SERIAL PRIMARY KEY,
name VARCHAR(255) NOT NULL,
description TEXT,
qr_code VARCHAR(100) UNIQUE NOT NULL,
parent_id INTEGER REFERENCES storage_locations(id),
level INTEGER NOT NULL DEFAULT 0,
path TEXT NOT NULL, -- Materialized path: "shelf1/drawer2/box3"
is_active BOOLEAN DEFAULT TRUE,
created_at TIMESTAMP DEFAULT NOW(),
updated_at TIMESTAMP DEFAULT NOW()
);
CREATE INDEX idx_storage_locations_parent_id ON storage_locations(parent_id);
CREATE INDEX idx_storage_locations_qr_code ON storage_locations(qr_code);
CREATE INDEX idx_storage_locations_path ON storage_locations USING gin(path gin_trgm_ops);
CREATE UNIQUE INDEX idx_storage_locations_name_parent ON storage_locations(name, parent_id);
```
### 2. Modified Components Table
```sql
-- Migration to add storage_location_id to components
ALTER TABLE components
ADD COLUMN storage_location_id INTEGER REFERENCES storage_locations(id),
ADD COLUMN legacy_position VARCHAR(255); -- Keep old position data for migration
-- Move existing position data to legacy_position
UPDATE components SET legacy_position = position;
```
## QR Code Format Design
### Hierarchical QR Code Strategy
To avoid confusion with multiple QR codes in the same image, use a hierarchical encoding strategy:
```
Format: SL:{level}:{unique_id}:{parent_path_hash}
Examples:
- Shelf: "SL:1:ABC123:ROOT"
- Drawer: "SL:2:DEF456:ABC123"
- Box: "SL:3:GHI789:DEF456"
```
### QR Code Components:
- **SL**: Storage Location prefix
- **Level**: Hierarchy level (1=shelf, 2=drawer, 3=box, etc.)
- **Unique ID**: Short alphanumeric code (6-8 chars)
- **Parent Hash**: Reference to parent location
## Multi-QR Code Detection Strategy
### 1. Spatial Filtering
```
When multiple QR codes detected:
1. Calculate distance between codes
2. If distance < threshold:
- Prefer higher hierarchy level (lower number)
- Present disambiguation UI
3. If distance > threshold:
- Allow user to tap/select desired code
```
### 2. Context-Aware Selection
```
Selection Priority:
1. Exact level match (if user scanning for specific level)
2. Deepest level in hierarchy (most specific location)
3. Recently used locations (user preference learning)
4. Manual disambiguation prompt
```
### 3. Visual Feedback
```
Camera Overlay:
- Draw bounding boxes around each detected QR code
- Color-code by hierarchy level
- Show location path preview on hover/tap
- Highlight "best match" with different color
```
## Implementation Components
### 1. Elixir Modules
#### Storage Location Schema
```elixir
defmodule ComponentsElixir.Inventory.StorageLocation do
use Ecto.Schema
import Ecto.Changeset
schema "storage_locations" do
field :name, :string
field :description, :string
field :qr_code, :string
field :level, :integer, default: 0
field :path, :string
field :is_active, :boolean, default: true
belongs_to :parent, __MODULE__
has_many :children, __MODULE__, foreign_key: :parent_id
has_many :components, Component
timestamps()
end
end
```
#### QR Code Generation
```elixir
defmodule ComponentsElixir.QRCode do
def generate_storage_qr(location) do
qr_data = "SL:#{location.level}:#{location.qr_code}:#{parent_hash(location)}"
# Use :qr_code library to generate QR image
:qr_code.encode(qr_data)
|> :qr_code.png()
end
def parse_storage_qr(qr_string) do
case String.split(qr_string, ":") do
["SL", level, code, parent] ->
{:ok, %{level: level, code: code, parent: parent}}
_ ->
{:error, :invalid_format}
end
end
end
```
### 2. Phoenix LiveView Components
#### QR Scanner Component
```elixir
defmodule ComponentsElixirWeb.QRScannerLive do
use ComponentsElixirWeb, :live_view
def mount(_params, _session, socket) do
socket =
socket
|> assign(:scanning, false)
|> assign(:detected_codes, [])
|> assign(:selected_location, nil)
|> allow_upload(:qr_scan,
accept: ~w(.jpg .jpeg .png),
max_entries: 1,
auto_upload: true)
{:ok, socket}
end
def handle_event("start_scan", _, socket) do
{:noreply, assign(socket, :scanning, true)}
end
def handle_event("qr_detected", %{"codes" => codes}, socket) do
parsed_codes = Enum.map(codes, &parse_and_resolve_location/1)
socket =
socket
|> assign(:detected_codes, parsed_codes)
|> maybe_auto_select_location(parsed_codes)
{:noreply, socket}
end
defp maybe_auto_select_location(socket, [single_code]) do
assign(socket, :selected_location, single_code)
end
defp maybe_auto_select_location(socket, multiple_codes) do
# Show disambiguation UI
assign(socket, :selected_location, nil)
end
end
```
### 3. JavaScript QR Detection
#### Camera Integration
```javascript
// assets/js/qr_scanner.js
import jsQR from "jsqr";
export const QRScanner = {
mounted() {
this.video = this.el.querySelector('video');
this.canvas = this.el.querySelector('canvas');
this.context = this.canvas.getContext('2d');
this.startCamera();
this.scanLoop();
},
async startCamera() {
try {
const stream = await navigator.mediaDevices.getUserMedia({
video: {
facingMode: 'environment', // Use back camera
width: { ideal: 1280 },
height: { ideal: 720 }
}
});
this.video.srcObject = stream;
} catch (err) {
console.error('Camera access denied:', err);
}
},
scanLoop() {
if (this.video.readyState === this.video.HAVE_ENOUGH_DATA) {
this.canvas.width = this.video.videoWidth;
this.canvas.height = this.video.videoHeight;
this.context.drawImage(this.video, 0, 0);
const imageData = this.context.getImageData(0, 0, this.canvas.width, this.canvas.height);
// Detect multiple QR codes
const codes = this.detectMultipleQRCodes(imageData);
if (codes.length > 0) {
this.pushEvent("qr_detected", { codes: codes });
}
}
requestAnimationFrame(() => this.scanLoop());
},
detectMultipleQRCodes(imageData) {
// Implementation for detecting multiple QR codes
// This is a simplified version - you'd need a more robust library
const detected = [];
// Scan in grid pattern to find multiple codes
const gridSize = 4;
const width = imageData.width / gridSize;
const height = imageData.height / gridSize;
for (let x = 0; x < gridSize; x++) {
for (let y = 0; y < gridSize; y++) {
const subImageData = this.getSubImageData(
imageData,
x * width,
y * height,
width,
height
);
const code = jsQR(subImageData.data, subImageData.width, subImageData.height);
if (code && this.isStorageLocationQR(code.data)) {
detected.push({
data: code.data,
location: { x: x * width, y: y * height },
corners: code.location
});
}
}
}
return this.filterDuplicates(detected);
},
isStorageLocationQR(data) {
return data.startsWith('SL:');
}
};
```
## User Experience Flow
### 1. Adding Components with QR Scan
```
1. User clicks "Add Component"
2. Position field shows camera icon
3. Click camera → QR scanner opens
4. Scan storage location QR code
5. If multiple codes detected:
- Show overlay with detected locations
- User taps to select specific location
6. Location path auto-filled: "Shelf A → Drawer 2 → Box 5"
7. Component saved with storage_location_id
```
### 2. Filtering by Storage Location
```
1. Component list shows location filter dropdown
2. Filter options show hierarchical tree:
├── Shelf A
│ ├── Drawer 1
│ │ ├── Box 1
│ │ └── Box 2
│ └── Drawer 2
└── Shelf B
3. Select any level to filter components
4. Breadcrumb shows: "Shelf A → Drawer 2" (23 components)
```
### 3. Location Management
```
1. New "Storage Locations" section in admin
2. Add/edit locations with auto QR generation
3. Print QR labels with location hierarchy
4. Bulk QR code generation for initial setup
```
## Handling Multiple QR Codes in Same Image
### Strategy 1: Spatial Separation
- Calculate euclidean distance between QR code centers
- If distance < 100px → show disambiguation
- If distance > 100px → allow selection by tap
### Strategy 2: Hierarchy Preference
- Always prefer deepest level (most specific)
- If same level → show all options
- Color-code by hierarchy level in UI
### Strategy 3: Machine Learning (Future)
- Learn user selection patterns
- Predict most likely intended QR code
- Still allow manual override
## Migration Strategy
### Phase 1: Add Storage Locations
1. Create migration for storage_locations table
2. Add storage_location_id to components
3. Create admin interface for location management
### Phase 2: QR Code Generation
1. Add QR code generation to location creation
2. Implement QR code printing/export functionality
3. Generate codes for existing locations
### Phase 3: QR Code Scanning
1. Add camera permissions and JavaScript QR scanner
2. Implement single QR code detection first
3. Add multi-QR detection and disambiguation
### Phase 4: Advanced Features
1. Location-based filtering and search
2. Bulk operations by location
3. Location analytics and optimization
## Technical Dependencies
### Elixir Dependencies
```elixir
# mix.exs
{:qr_code, "~> 3.1"}, # QR code generation
{:image, "~> 0.37"}, # Image processing
{:ex_image_info, "~> 0.2.4"} # Image metadata
```
### JavaScript Dependencies
```json
// package.json
{
"jsqr": "^1.4.0",
"qr-scanner": "^1.4.2"
}
```
## Database Indexes for Performance
```sql
-- Fast location lookups
CREATE INDEX idx_components_storage_location_id ON components(storage_location_id);
-- Hierarchical queries
CREATE INDEX idx_storage_locations_path_gin ON storage_locations USING gin(path gin_trgm_ops);
-- QR code uniqueness and fast lookup
CREATE UNIQUE INDEX idx_storage_locations_qr_code ON storage_locations(qr_code);
```
This design provides a robust foundation for QR code-based storage management while handling the complexity of multiple codes in the same image through spatial analysis and user interaction patterns.