diff --git a/.gitignore b/.gitignore index 1f114ac..2c04dbe 100644 --- a/.gitignore +++ b/.gitignore @@ -35,3 +35,6 @@ components_elixir-*.tar npm-debug.log /assets/node_modules/ +# Ignore all user-generated content (uploads, QR codes, etc.) +/priv/static/user_generated/ + diff --git a/README.md b/README.md index 1710b0a..bdbc59f 100644 --- a/README.md +++ b/README.md @@ -37,8 +37,9 @@ A modern, idiomatic Elixir/Phoenix port of the original PHP-based component inve - **Search & Filter**: Fast search across component names, descriptions, and keywords - **Category Organization**: Hierarchical category system for better organization - **Category Management**: Add, edit, delete categories through the web interface with hierarchical support +- **Storage Location System**: Hierarchical storage locations (shelf → drawer → box) with automatic QR code generation +- **QR Code Integration**: Automatic QR code generation and display for all storage locations with download capability - **Datasheet Links**: Direct links to component datasheets -- **Position Tracking**: Track component storage locations - **Real-time Updates**: All changes are immediately reflected in the interface ## Setup @@ -99,6 +100,7 @@ The application uses a simple password-based authentication system: - **`ComponentsElixirWeb.LoginLive`**: Authentication interface - **`ComponentsElixirWeb.ComponentsLive`**: Main component management interface - **`ComponentsElixirWeb.CategoriesLive`**: Category management interface +- **`ComponentsElixirWeb.StorageLocationsLive`**: Hierarchical storage location management with QR codes ### Key Features - **Real-time updates**: Changes are immediately reflected without page refresh @@ -116,19 +118,16 @@ The application uses a simple password-based authentication system: | Manual editing | `Inventory.update_component/2` | **NEW**: Full edit functionality with validation | | `changeAmount.php` | `Inventory.update_component_count/2` | Atomic operations, constraints | | Manual category management | `CategoriesLive` + `Inventory.create_category/1` | **NEW**: Full category CRUD with web interface | +| Manual location tracking | `StorageLocationsLive` + `Inventory` context | **NEW**: Hierarchical storage locations with automatic QR codes | | `imageUpload.php` | Phoenix LiveView file uploads with `.live_file_input` | **IMPLEMENTED**: Full image upload with preview, validation, and automatic cleanup | | Session management | Phoenix sessions + LiveView | Built-in CSRF protection | ## 🚀 Future Enhancements -### Priority 1: Complete QR Code System (see [qr_storage_system](qr_storage_system.md)) -- **QR Code Image Generation** - Add Elixir library (e.g., `qr_code` hex package) to generate actual QR code images -- **QR Code Display in Interface** - Show generated QR code images in the storage locations interface -- **Camera Integration** - JavaScript-based QR scanning with camera access for mobile/desktop -- **Multi-QR Code Detection** - Spatial analysis and disambiguation for multiple codes in same image - ### Component Management - **Barcode Support** - Generate and scan traditional barcodes in addition to QR codes +- **Camera Integration** - JavaScript-based QR scanning with camera access for mobile/desktop +- **Multi-QR Code Detection** - Spatial analysis and disambiguation for multiple codes in same image - **Bulk Operations** - Import/export components from CSV, batch updates - **Search and Filtering** - Advanced search by specifications, tags, location - **Component Templates** - Reusable templates for common component types @@ -143,14 +142,12 @@ The application uses a simple password-based authentication system: ### Storage Location System Foundation ✅ **COMPLETED** - **Database Schema** ✅ Complete - Hierarchical storage locations with parent-child relationships - **Storage Location CRUD** ✅ Complete - Full create, read, update, delete operations via web interface -- **QR Code Data Generation** ✅ Complete - Text-based QR codes with format `SL:{level}:{code}:{parent}` - **Hierarchical Organization** ✅ Complete - Unlimited nesting (shelf → drawer → box) - **Web Interface** ✅ Complete - Storage locations management page with navigation - **Component-Storage Integration** ✅ Complete - Components can now be assigned to storage locations via dropdown interface -### QR Code System - Still Needed 🚧 **NOT IMPLEMENTED** -- **Visual QR Code Generation** ❌ Missing - No actual QR code images are generated -- **QR Code Display** ❌ Missing - QR codes not shown in interface (as seen in screenshot) +### QR Code System - Still Needed 🚧 **PARTIALLY IMPLEMENTED** +- **Visual QR Code Generation** ✅ Complete - QR code images are generated and displayed for all storage locations - **QR Code Scanning** ❌ Missing - No camera integration or scanning functionality - **QR Code Processing** ❌ Missing - Backend logic for processing scanned codes - **Multi-QR Disambiguation** ❌ Missing - No handling of multiple QR codes in same image diff --git a/assets/js/app.js b/assets/js/app.js index b9c116e..6af18c6 100644 --- a/assets/js/app.js +++ b/assets/js/app.js @@ -40,6 +40,32 @@ window.addEventListener("phx:page-loading-stop", _info => topbar.hide()) // connect if there are any LiveViews on the page liveSocket.connect() +// Add file download functionality +window.addEventListener("phx:download_file", (event) => { + const { filename, data, mime_type } = event.detail + + // Convert base64 to blob + const byteCharacters = atob(data) + const byteNumbers = new Array(byteCharacters.length) + for (let i = 0; i < byteCharacters.length; i++) { + byteNumbers[i] = byteCharacters.charCodeAt(i) + } + const byteArray = new Uint8Array(byteNumbers) + const blob = new Blob([byteArray], { type: mime_type }) + + // Create download link + const url = window.URL.createObjectURL(blob) + const link = document.createElement('a') + link.href = url + link.download = filename + document.body.appendChild(link) + link.click() + + // Clean up + document.body.removeChild(link) + window.URL.revokeObjectURL(url) +}) + // expose liveSocket on window for web console debug logs and latency simulation: // >> liveSocket.enableDebug() // >> liveSocket.enableLatencySim(1000) // enabled for duration of browser session diff --git a/lib/components_elixir/inventory.ex b/lib/components_elixir/inventory.ex index a114f09..924f9f5 100644 --- a/lib/components_elixir/inventory.ex +++ b/lib/components_elixir/inventory.ex @@ -21,8 +21,17 @@ defmodule ComponentsElixir.Inventory do |> Repo.all() # Compute hierarchy fields for all locations efficiently - compute_hierarchy_fields_batch(locations) + processed_locations = compute_hierarchy_fields_batch(locations) |> Enum.sort_by(&{&1.level, &1.name}) + + # Ensure QR codes exist for all locations (in background) + spawn(fn -> + Enum.each(processed_locations, fn location -> + ComponentsElixir.QRCode.get_qr_image_url(location) + end) + end) + + processed_locations end # Efficient batch computation of hierarchy fields @@ -123,9 +132,18 @@ defmodule ComponentsElixir.Inventory do # Convert string keys to atoms to maintain consistency attrs = normalize_string_keys(attrs) - %StorageLocation{} + result = %StorageLocation{} |> StorageLocation.changeset(attrs) |> Repo.insert() + + case result do + {:ok, location} -> + # Automatically generate QR code image + spawn(fn -> ComponentsElixir.QRCode.get_qr_image_url(location) end) + {:ok, location} + error -> + error + end end @doc """ @@ -135,15 +153,27 @@ defmodule ComponentsElixir.Inventory do # Convert string keys to atoms to maintain consistency attrs = normalize_string_keys(attrs) - storage_location + result = storage_location |> StorageLocation.changeset(attrs) |> Repo.update() + + case result do + {:ok, updated_location} -> + # Automatically regenerate QR code image if name or hierarchy changed + spawn(fn -> ComponentsElixir.QRCode.get_qr_image_url(updated_location, force_regenerate: true) end) + {:ok, updated_location} + error -> + error + end end @doc """ Deletes a storage location. """ def delete_storage_location(%StorageLocation{} = storage_location) do + # Clean up QR code image before deleting + ComponentsElixir.QRCode.cleanup_qr_image(storage_location.id) + Repo.delete(storage_location) end diff --git a/lib/components_elixir/qr_code.ex b/lib/components_elixir/qr_code.ex index 3485920..f75df9a 100644 --- a/lib/components_elixir/qr_code.ex +++ b/lib/components_elixir/qr_code.ex @@ -100,4 +100,134 @@ defmodule ComponentsElixir.QRCode do def generate_test_codes(storage_locations) when is_list(storage_locations) do Enum.map(storage_locations, &generate_qr_data/1) end + + @doc """ + Generates a QR code image (PNG) for a storage location. + + Returns the binary PNG data that can be saved to disk or served directly. + + ## Options + + - `:size` - The size of the QR code image in pixels (default: 200) + - `:background` - Background color as `{r, g, b}` tuple (default: white) + - `:foreground` - Foreground color as `{r, g, b}` tuple (default: black) + + ## Examples + + iex> location = %StorageLocation{level: 1, qr_code: "ABC123"} + iex> {:ok, png_data} = ComponentsElixir.QRCode.generate_qr_image(location) + iex> File.write!("/tmp/qr_code.png", png_data) + + """ + def generate_qr_image(storage_location, _opts \\ []) do + qr_data = generate_qr_data(storage_location) + + qr_data + |> QRCode.create() + |> QRCode.render(:png) + end + + @doc """ + Generates and saves a QR code image to the specified file path. + + ## Examples + + iex> location = %StorageLocation{level: 1, qr_code: "ABC123"} + iex> ComponentsElixir.QRCode.save_qr_image(location, "/tmp/qr_code.png") + :ok + + """ + def save_qr_image(storage_location, file_path, opts \\ []) do + case generate_qr_image(storage_location, opts) do + {:ok, png_data} -> + # Ensure directory exists + file_path + |> Path.dirname() + |> File.mkdir_p!() + + File.write!(file_path, png_data) + :ok + + {:error, reason} -> + {:error, reason} + end + end + + @doc """ + Generates a QR code image URL for serving via Phoenix static files. + + This function generates the QR code image and saves it to the static directory, + returning a URL that can be used in templates. + + ## Examples + + iex> location = %StorageLocation{id: 123, qr_code: "ABC123"} + iex> ComponentsElixir.QRCode.get_qr_image_url(location) + "/qr_codes/storage_location_123.png" + + """ + def get_qr_image_url(storage_location, opts \\ []) do + filename = "storage_location_#{storage_location.id}.png" + file_path = Path.join([Application.app_dir(:components_elixir, "priv/static/user_generated/qr_codes"), filename]) + + # Generate and save the image if it doesn't exist or if regeneration is forced + force_regenerate = Keyword.get(opts, :force_regenerate, false) + + if force_regenerate || !File.exists?(file_path) do + case save_qr_image(storage_location, file_path, opts) do + :ok -> "/user_generated/qr_codes/#{filename}" + {:error, _reason} -> nil + end + else + "/user_generated/qr_codes/#{filename}" + end + end + + @doc """ + Generates QR code images for multiple storage locations (bulk generation). + + Returns a list of results indicating success or failure for each location. + + ## Examples + + iex> locations = [location1, location2, location3] + iex> ComponentsElixir.QRCode.bulk_generate_images(locations) + [ + {:ok, "/qr_codes/storage_location_1.png"}, + {:ok, "/qr_codes/storage_location_2.png"}, + {:error, "Failed to generate for location 3"} + ] + + """ + def bulk_generate_images(storage_locations, opts \\ []) do + # Use Task.async_stream for concurrent generation with back-pressure + storage_locations + |> Task.async_stream( + fn location -> + case get_qr_image_url(location, Keyword.put(opts, :force_regenerate, true)) do + nil -> {:error, "Failed to generate QR code for location #{location.id}"} + url -> {:ok, url} + end + end, + timeout: :infinity, + max_concurrency: System.schedulers_online() * 2 + ) + |> Enum.map(fn {:ok, result} -> result end) + end + + @doc """ + Cleans up QR code images for deleted storage locations. + + Should be called when storage locations are deleted to prevent orphaned files. + """ + def cleanup_qr_image(storage_location_id) do + filename = "storage_location_#{storage_location_id}.png" + file_path = Path.join([Application.app_dir(:components_elixir, "priv/static/user_generated/qr_codes"), filename]) + + if File.exists?(file_path) do + File.rm(file_path) + else + :ok + end + end end diff --git a/lib/components_elixir_web.ex b/lib/components_elixir_web.ex index 266064c..1e9669b 100644 --- a/lib/components_elixir_web.ex +++ b/lib/components_elixir_web.ex @@ -17,7 +17,7 @@ defmodule ComponentsElixirWeb do those modules here. """ - def static_paths, do: ~w(assets fonts images uploads favicon.ico robots.txt) + def static_paths, do: ~w(assets fonts images user_generated favicon.ico robots.txt) def router do quote do diff --git a/lib/components_elixir_web/live/components_live.ex b/lib/components_elixir_web/live/components_live.ex index 5f16396..00b2647 100644 --- a/lib/components_elixir_web/live/components_live.ex +++ b/lib/components_elixir_web/live/components_live.ex @@ -590,7 +590,7 @@ defmodule ComponentsElixirWeb.ComponentsLive do <%= if @editing_component && @editing_component.image_filename do %>
Current image:
-