defmodule ComponentsElixirWeb.StorageLocationsLive do @moduledoc """ LiveView for managing storage locations and QR codes. """ use ComponentsElixirWeb, :live_view alias ComponentsElixir.{Inventory, Auth} alias ComponentsElixir.Inventory.StorageLocation alias ComponentsElixir.QRCode @impl true def mount(_params, session, socket) do # Check authentication unless Auth.authenticated?(session) do {:ok, socket |> push_navigate(to: ~p"/login")} else storage_locations = list_storage_locations() {:ok, socket |> assign(:session, session) |> assign(:storage_locations, storage_locations) |> assign(:show_add_form, false) |> assign(:show_edit_form, false) |> assign(:editing_location, nil) |> assign(:form, nil) |> assign(:qr_scanner_open, false) |> assign(:scanned_codes, []) |> assign(:page_title, "Storage Location Management")} end end @impl true def handle_event("show_add_form", _params, socket) do changeset = Inventory.change_storage_location(%StorageLocation{}) form = to_form(changeset) {:noreply, socket |> assign(:show_add_form, true) |> assign(:form, form)} end def handle_event("hide_add_form", _params, socket) do {:noreply, socket |> assign(:show_add_form, false) |> assign(:form, nil)} end def handle_event("show_edit_form", %{"id" => id}, socket) do location = Inventory.get_storage_location!(id) # Create a changeset with current values forced into changes for proper form display changeset = Inventory.change_storage_location(location, %{ name: location.name, description: location.description, parent_id: location.parent_id }) |> Ecto.Changeset.force_change(:parent_id, location.parent_id) form = to_form(changeset) {:noreply, socket |> assign(:show_edit_form, true) |> assign(:editing_location, location) |> assign(:form, form)} end def handle_event("hide_edit_form", _params, socket) do {:noreply, socket |> assign(:show_edit_form, false) |> assign(:editing_location, nil) |> assign(:form, nil)} end def handle_event("save_location", %{"storage_location" => location_params}, socket) do case Inventory.create_storage_location(location_params) do {:ok, _location} -> {:noreply, socket |> put_flash(:info, "Storage location created successfully") |> assign(:show_add_form, false) |> assign(:form, nil) |> reload_storage_locations()} {:error, changeset} -> {:noreply, assign(socket, :form, to_form(changeset))} end end def handle_event("save_edit", %{"storage_location" => location_params}, socket) do case Inventory.update_storage_location(socket.assigns.editing_location, location_params) do {:ok, _location} -> {:noreply, socket |> put_flash(:info, "Storage location updated successfully") |> assign(:show_edit_form, false) |> assign(:editing_location, nil) |> assign(:form, nil) |> reload_storage_locations()} {:error, changeset} -> {:noreply, assign(socket, :form, to_form(changeset))} end end def handle_event("delete_location", %{"id" => id}, socket) do location = Inventory.get_storage_location!(id) case Inventory.delete_storage_location(location) do {:ok, _deleted_location} -> {:noreply, socket |> put_flash(:info, "Storage location deleted successfully") |> reload_storage_locations()} {:error, _changeset} -> {:noreply, put_flash(socket, :error, "Cannot delete storage location - it may have components assigned or child locations")} end end def handle_event("open_qr_scanner", _params, socket) do {:noreply, assign(socket, :qr_scanner_open, true)} end def handle_event("close_qr_scanner", _params, socket) do {:noreply, assign(socket, :qr_scanner_open, false)} end def handle_event("qr_scanned", %{"code" => code}, socket) do case QRCode.parse_qr_data(code) do {:ok, parsed} -> case Inventory.get_storage_location_by_qr_code(parsed.code) do nil -> {:noreply, put_flash(socket, :error, "Storage location not found for QR code: #{code}")} location -> scanned_codes = [%{code: code, location: location} | socket.assigns.scanned_codes] {:noreply, socket |> assign(:scanned_codes, scanned_codes) |> put_flash(:info, "Scanned: #{location.path}")} end {:error, reason} -> {:noreply, put_flash(socket, :error, "Invalid QR code: #{reason}")} end end def handle_event("clear_scanned", _params, socket) do {:noreply, assign(socket, :scanned_codes, [])} end def handle_event("download_qr", %{"id" => id}, socket) do case Inventory.get_storage_location!(id) do location -> case QRCode.generate_qr_image(location) do {:ok, png_data} -> filename = "#{location.name |> String.replace(" ", "_")}_QR.png" # Send file download to browser {:noreply, socket |> push_event("download_file", %{ filename: filename, data: Base.encode64(png_data), mime_type: "image/png" })} {:error, _reason} -> {:noreply, put_flash(socket, :error, "Failed to generate QR code")} end end end defp reload_storage_locations(socket) do storage_locations = list_storage_locations() assign(socket, :storage_locations, storage_locations) end defp parent_location_options(storage_locations, editing_location_id \\ nil) do available_locations = storage_locations |> Enum.reject(fn loc -> loc.id == editing_location_id || (editing_location_id && is_descendant?(storage_locations, loc.id, editing_location_id)) end) |> Enum.map(fn location -> {location_display_name(location), location.id} end) [{"No parent (Root location)", nil}] ++ available_locations end defp is_descendant?(storage_locations, descendant_id, ancestor_id) do # Check if descendant_id is a descendant of ancestor_id descendant = Enum.find(storage_locations, fn loc -> loc.id == descendant_id end) case descendant do nil -> false %{parent_id: nil} -> false %{parent_id: parent_id} when parent_id == ancestor_id -> true %{parent_id: parent_id} -> is_descendant?(storage_locations, parent_id, ancestor_id) end end defp location_display_name(location) do if location.path do # Convert path from "Shelf A/Drawer 2/Box 1" to "Shelf A > Drawer 2 > Box 1" location.path |> String.split("/") |> Enum.join(" > ") else location.name end end defp root_storage_locations(storage_locations) do Enum.filter(storage_locations, fn loc -> is_nil(loc.parent_id) end) end defp child_storage_locations(storage_locations, parent_id) do Enum.filter(storage_locations, fn loc -> loc.parent_id == parent_id end) end defp count_components_in_location(location_id) do Inventory.count_components_in_storage_location(location_id) end defp get_qr_image_url(location) do QRCode.get_qr_image_url(location) end # Component for rendering individual storage location items with QR code support defp location_item(assigns) do # Calculate margin based on depth (0 = no margin, 1+ = incremental margin) margin_left = if assigns.depth == 0, do: 0, else: 32 + assigns.depth * 16 # Determine border style based on depth border_class = if assigns.depth > 0, do: "border-l-2 border-gray-200 pl-6", else: "" # Icon size and button size based on depth {icon_size, button_size, text_size, title_tag} = case assigns.depth do 0 -> {"w-5 h-5", "p-2", "text-lg", "h3"} 1 -> {"w-4 h-4", "p-1.5", "text-base", "h4"} _ -> {"w-3 h-3", "p-1", "text-sm", "h5"} end # Different icons based on level - QR code is always present for storage locations icon_name = case assigns.depth do 0 -> "hero-building-office" # Shelf/Room 1 -> "hero-archive-box" # Drawer/Cabinet _ -> "hero-cube" # Box/Container end assigns = assigns |> assign(:margin_left, margin_left) |> assign(:border_class, border_class) |> assign(:icon_size, icon_size) |> assign(:button_size, button_size) |> assign(:text_size, text_size) |> assign(:title_tag, title_tag) |> assign(:icon_name, icon_name) |> assign(:children, child_storage_locations(assigns.storage_locations, assigns.location.id)) ~H"""
{@location.description}
<% end %>{count_components_in_location(@location.id)} components
<%= if @location.qr_code do %> QR: {@location.qr_code} <% end %>Camera QR scanner would go here
In a real implementation, this would use JavaScript QR scanning
Test with sample codes:
Manage your physical storage locations and QR codes
Get started by creating your first storage location.