diff --git a/lib/components_elixir/auth.ex b/lib/components_elixir/auth.ex index 12320e7..7dccb5c 100644 --- a/lib/components_elixir/auth.ex +++ b/lib/components_elixir/auth.ex @@ -62,6 +62,6 @@ defmodule ComponentsElixir.Auth do defp put_session_value(%Phoenix.LiveView.Socket{} = socket, key, value) do session = Map.put(socket.assigns[:session] || %{}, key, value) - Phoenix.LiveView.assign(socket, session: session) + %{socket | assigns: Map.put(socket.assigns, :session, session)} end end diff --git a/lib/components_elixir/inventory.ex b/lib/components_elixir/inventory.ex index 0109d9b..a114f09 100644 --- a/lib/components_elixir/inventory.ex +++ b/lib/components_elixir/inventory.ex @@ -348,6 +348,15 @@ defmodule ComponentsElixir.Inventory do |> Repo.aggregate(:count, :id) end + @doc """ + Counts components in a specific storage location. + """ + def count_components_in_storage_location(storage_location_id) do + Component + |> where([c], c.storage_location_id == ^storage_location_id) + |> Repo.aggregate(:count, :id) + end + @doc """ Increment component stock count. """ diff --git a/lib/components_elixir_web/live/storage_locations_live.ex b/lib/components_elixir_web/live/storage_locations_live.ex index ef8941e..4eb136f 100644 --- a/lib/components_elixir_web/live/storage_locations_live.ex +++ b/lib/components_elixir_web/live/storage_locations_live.ex @@ -4,114 +4,120 @@ defmodule ComponentsElixirWeb.StorageLocationsLive do """ use ComponentsElixirWeb, :live_view - alias ComponentsElixir.Inventory + alias ComponentsElixir.{Inventory, Auth} alias ComponentsElixir.Inventory.StorageLocation alias ComponentsElixir.QRCode @impl true - def mount(_params, _session, socket) do - socket = - socket - |> assign(:storage_locations, list_storage_locations()) - |> assign(:form, to_form(%{})) - |> assign(:show_form, false) - |> assign(:edit_location, nil) - |> assign(:qr_scanner_open, false) - |> assign(:scanned_codes, []) + 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} - end - - @impl true - def handle_params(params, _url, socket) do - {:noreply, apply_action(socket, socket.assigns.live_action, params)} - end - - defp apply_action(socket, :index, _params) do - socket - |> assign(:page_title, "Storage Locations") - |> assign(:storage_location, %StorageLocation{}) - end - - defp apply_action(socket, :new, _params) do - socket - |> assign(:page_title, "New Storage Location") - |> assign(:storage_location, %StorageLocation{}) - |> assign(:show_form, true) - end - - defp apply_action(socket, :edit, %{"id" => id}) do - location = Inventory.get_storage_location!(id) - - socket - |> assign(:page_title, "Edit Storage Location") - |> assign(:storage_location, location) - |> assign(:edit_location, location) - |> assign(:show_form, true) - |> assign(:form, to_form(Inventory.change_storage_location(location))) - end - - @impl true - def handle_event("new", _params, socket) do - {:noreply, - socket - |> assign(:show_form, true) - |> assign(:storage_location, %StorageLocation{}) - |> assign(:edit_location, nil) - |> assign(:form, to_form(Inventory.change_storage_location(%StorageLocation{})))} - end - - def handle_event("cancel", _params, socket) do - {:noreply, - socket - |> assign(:show_form, false) - |> assign(:edit_location, nil) - |> push_patch(to: ~p"/storage_locations")} - end - - def handle_event("validate", %{"storage_location" => params}, socket) do - # Normalize parent_id for validation too - normalized_params = - case Map.get(params, "parent_id") do - "" -> Map.put(params, "parent_id", nil) - value -> Map.put(params, "parent_id", value) - end - - changeset = - case socket.assigns.edit_location do - nil -> Inventory.change_storage_location(%StorageLocation{}, normalized_params) - location -> Inventory.change_storage_location(location, normalized_params) - end - - {:noreply, assign(socket, :form, to_form(changeset, action: :validate))} - end - - def handle_event("save", %{"storage_location" => params}, socket) do - # Normalize parent_id for consistency - normalized_params = - case Map.get(params, "parent_id") do - "" -> Map.put(params, "parent_id", nil) - value -> Map.put(params, "parent_id", value) - end - - case socket.assigns.edit_location do - nil -> create_storage_location(socket, normalized_params) - location -> update_storage_location(socket, location, normalized_params) + {: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 - def handle_event("delete", %{"id" => id}, socket) do + @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, _} -> + {:ok, _deleted_location} -> {:noreply, socket |> put_flash(:info, "Storage location deleted successfully") - |> assign(:storage_locations, list_storage_locations())} + |> reload_storage_locations()} - {:error, _} -> - {:noreply, put_flash(socket, :error, "Unable to delete storage location")} + {:error, _changeset} -> + {:noreply, put_flash(socket, :error, "Cannot delete storage location - it may have components assigned or child locations")} end end @@ -148,61 +154,416 @@ defmodule ComponentsElixirWeb.StorageLocationsLive do {:noreply, assign(socket, :scanned_codes, [])} end - defp create_storage_location(socket, params) do - case Inventory.create_storage_location(params) do - {:ok, _location} -> - {:noreply, - socket - |> put_flash(:info, "Storage location created successfully") - |> assign(:show_form, false) - |> assign(:storage_locations, list_storage_locations())} + defp reload_storage_locations(socket) do + storage_locations = list_storage_locations() + assign(socket, :storage_locations, storage_locations) + end - {:error, %Ecto.Changeset{} = changeset} -> - {:noreply, assign(socket, :form, to_form(changeset))} + 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 update_storage_location(socket, location, params) do - case Inventory.update_storage_location(location, params) do - {:ok, _location} -> - {:noreply, - socket - |> put_flash(:info, "Storage location updated successfully") - |> assign(:show_form, false) - |> assign(:edit_location, nil) - |> assign(:storage_locations, list_storage_locations()) - |> push_patch(to: ~p"/storage_locations")} - - {:error, %Ecto.Changeset{} = changeset} -> - {:noreply, assign(socket, :form, to_form(changeset))} + 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 + + # 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. +
+Manage your physical storage locations and QR codes
-Camera QR scanner would go here
-In a real implementation, this would use JavaScript QR scanning
- - -Test with sample codes:
- - - -| - Location - | -- Level - | -- QR Code - | -- Description - | -- Actions - | -
|---|---|---|---|---|
|
-
-
-
- <%= location.path %>
-
-
-
- DEBUG - ID: <%= location.id %>, Parent: <%= inspect(location.parent_id) %>, Level: <%= location.level %>
-
- |
- - - <%= format_level(location.level) %> - - | -
-
- <%= location.qr_code %>
-
- |
-
-
- <%= location.description %>
-
- |
-
-
- <.link
- patch={~p"/storage_locations/#{location.id}/edit"}
- class="text-indigo-600 hover:text-indigo-900"
- >
- Edit
-
-
-
- |
-
No storage locations yet. Create one to get started!
-- Here are some sample QR codes generated for your existing storage locations: -
-
- <%= ComponentsElixir.QRCode.generate_qr_data(location) %>
-
-