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"""
0 && "mt-4"]} style={"margin-left: #{@margin_left}px"}>
<.icon name={@icon_name} class={"#{@icon_size} #{if @depth == 0, do: "text-blue-500", else: "text-gray-400"}"} />
<%= if @title_tag == "h3" do %>

{@location.name}

<% else %>

{@location.name}

<% end %> <%= if @location.description do %>

{@location.description}

<% end %>

{count_components_in_location(@location.id)} components

<%= if @location.qr_code do %> QR: {@location.qr_code} <% end %>
<%= if @location.qr_code do %>
<%= if get_qr_image_url(@location) do %>
{"QR
<% else %>
<.icon name="hero-qr-code" class="w-8 h-8 text-gray-400" />
<% end %>
<% end %>
<%= for child <- @children do %> <.location_item location={child} storage_locations={@storage_locations} depth={@depth + 1} /> <% end %>
""" end defp list_storage_locations do Inventory.list_storage_locations() end @impl true def render(assigns) do ~H"""
<.link navigate={~p"/"} class="text-gray-500 hover:text-gray-700" > <.icon name="hero-arrow-left" class="w-5 h-5" />

Storage Location Management

<.link navigate={~p"/"} class="inline-flex items-center px-4 py-2 border border-gray-300 text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500" > <.icon name="hero-cube-transparent" class="w-4 h-4 mr-2" /> Components
<%= if @show_add_form do %>

Add New Storage Location

<.form for={@form} phx-submit="save_location" class="space-y-4">
<.input field={@form[:name]} type="text" required />
<.input field={@form[:parent_id]} type="select" options={parent_location_options(@storage_locations)} />
<.input field={@form[:description]} type="textarea" />
<% end %> <%= if @show_edit_form do %>

Edit Storage Location

<.form for={@form} phx-submit="save_edit" class="space-y-4">
<.input field={@form[:name]} type="text" required />
<.input field={@form[:parent_id]} type="select" options={parent_location_options(@storage_locations, @editing_location.id)} />
<.input field={@form[:description]} type="textarea" />
<% end %> <%= if @qr_scanner_open do %>

QR Code Scanner

<.icon name="hero-qr-code" class="mx-auto h-12 w-12 text-gray-400" />

Camera QR scanner would go here

In a real implementation, this would use JavaScript QR scanning

Test with sample codes:

<% end %> <%= if length(@scanned_codes) > 0 do %>

Recently Scanned

{location_display_name(scan.location)} ({scan.code})
Level {scan.location.level}
<% end %>

Storage Location Hierarchy

Manage your physical storage locations and QR codes

<%= if Enum.empty?(@storage_locations) do %>
<.icon name="hero-building-office" class="mx-auto h-12 w-12 text-gray-400" />

No storage locations

Get started by creating your first storage location.

<% else %>
<%= for location <- root_storage_locations(@storage_locations) do %>
<.location_item location={location} storage_locations={@storage_locations} depth={0} />
<% end %>
<% end %>
""" end end