feat(elixir): QR code generate & download function

This commit is contained in:
Schuwi
2025-09-14 16:35:06 +02:00
parent 1b498d286d
commit 788ad54724
10 changed files with 290 additions and 46 deletions

View File

@@ -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

View File

@@ -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