384 lines
10 KiB
Markdown
384 lines
10 KiB
Markdown
# 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. |