feat(elixir): storage location system
This commit is contained in:
384
design_docs/qr_storage_system.md
Normal file
384
design_docs/qr_storage_system.md
Normal 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.
|
||||
Reference in New Issue
Block a user