diff --git a/lib/components_elixir/inventory.ex b/lib/components_elixir/inventory.ex index 2bcb378..ae6d3c0 100644 --- a/lib/components_elixir/inventory.ex +++ b/lib/components_elixir/inventory.ex @@ -15,14 +15,16 @@ defmodule ComponentsElixir.Inventory do """ def list_storage_locations do # Get all locations with preloaded parents in a single query - locations = StorageLocation - |> order_by([sl], [asc: sl.name]) - |> preload(:parent) - |> Repo.all() + locations = + StorageLocation + |> order_by([sl], asc: sl.name) + |> preload(:parent) + |> Repo.all() # Compute hierarchy fields for all locations efficiently - processed_locations = compute_hierarchy_fields_batch(locations) - |> Enum.sort_by(&{&1.level, &1.name}) + processed_locations = + compute_hierarchy_fields_batch(locations) + |> Enum.sort_by(&{&1.level, &1.name}) # Ensure AprilTag SVGs exist for all locations spawn(fn -> @@ -46,24 +48,35 @@ defmodule ComponentsElixir.Inventory do end defp compute_level_efficient(%{parent_id: nil}, _location_map, _depth), do: 0 + defp compute_level_efficient(%{parent_id: parent_id}, location_map, depth) when depth < 10 do case Map.get(location_map, parent_id) do - nil -> 0 # Orphaned record + # Orphaned record + nil -> 0 parent -> 1 + compute_level_efficient(parent, location_map, depth + 1) end end - defp compute_level_efficient(_location, _location_map, _depth), do: 0 # Prevent infinite recursion + + # Prevent infinite recursion + defp compute_level_efficient(_location, _location_map, _depth), do: 0 defp compute_path_efficient(%{parent_id: nil, name: name}, _location_map, _depth), do: name - defp compute_path_efficient(%{parent_id: parent_id, name: name}, location_map, depth) when depth < 10 do + + defp compute_path_efficient(%{parent_id: parent_id, name: name}, location_map, depth) + when depth < 10 do case Map.get(location_map, parent_id) do - nil -> name # Orphaned record + # Orphaned record + nil -> + name + parent -> parent_path = compute_path_efficient(parent, location_map, depth + 1) "#{parent_path}/#{name}" end end - defp compute_path_efficient(%{name: name}, _location_map, _depth), do: name # Prevent infinite recursion + + # Prevent infinite recursion + defp compute_path_efficient(%{name: name}, _location_map, _depth), do: name @doc """ Returns the list of root storage locations (no parent). @@ -71,7 +84,7 @@ defmodule ComponentsElixir.Inventory do def list_root_storage_locations do StorageLocation |> where([sl], is_nil(sl.parent_id)) - |> order_by([sl], [asc: sl.name]) + |> order_by([sl], asc: sl.name) |> Repo.all() end @@ -79,9 +92,10 @@ defmodule ComponentsElixir.Inventory do Gets a single storage location with computed hierarchy fields. """ def get_storage_location!(id) do - location = StorageLocation - |> preload(:parent) - |> Repo.get!(id) + location = + StorageLocation + |> preload(:parent) + |> Repo.get!(id) # Compute hierarchy fields level = compute_level_for_single(location) @@ -91,6 +105,7 @@ defmodule ComponentsElixir.Inventory do # Simple computation for single location (allows DB queries) defp compute_level_for_single(%{parent_id: nil}), do: 0 + defp compute_level_for_single(%{parent_id: parent_id}) do case Repo.get(StorageLocation, parent_id) do nil -> 0 @@ -99,6 +114,7 @@ defmodule ComponentsElixir.Inventory do end defp compute_path_for_single(%{parent_id: nil, name: name}), do: name + defp compute_path_for_single(%{parent_id: parent_id, name: name}) do case Repo.get(StorageLocation, parent_id) do nil -> name @@ -115,7 +131,9 @@ defmodule ComponentsElixir.Inventory do |> preload(:parent) |> Repo.one() |> case do - nil -> nil + nil -> + nil + location -> level = compute_level_for_single(location) path = compute_path_for_single(location) @@ -130,13 +148,15 @@ defmodule ComponentsElixir.Inventory do # Convert string keys to atoms to maintain consistency attrs = normalize_string_keys(attrs) - result = %StorageLocation{} - |> StorageLocation.changeset(attrs) - |> Repo.insert() + result = + %StorageLocation{} + |> StorageLocation.changeset(attrs) + |> Repo.insert() case result do {:ok, location} -> {:ok, location} + error -> error end @@ -149,13 +169,15 @@ defmodule ComponentsElixir.Inventory do # Convert string keys to atoms to maintain consistency attrs = normalize_string_keys(attrs) - result = storage_location - |> StorageLocation.changeset(attrs) - |> Repo.update() + result = + storage_location + |> StorageLocation.changeset(attrs) + |> Repo.update() case result do {:ok, updated_location} -> {:ok, updated_location} + error -> error end @@ -182,12 +204,14 @@ defmodule ComponentsElixir.Inventory do case get_storage_location_by_apriltag_id(apriltag_id) do nil -> {:error, :not_found} + location -> - {:ok, %{ - type: :storage_location, - location: location, - apriltag_id: apriltag_id - }} + {:ok, + %{ + type: :storage_location, + location: location, + apriltag_id: apriltag_id + }} end end @@ -195,6 +219,7 @@ defmodule ComponentsElixir.Inventory do Computes the path for a storage location (for display purposes). """ def compute_storage_location_path(nil), do: nil + def compute_storage_location_path(%StorageLocation{} = location) do compute_path_for_single(location) end @@ -205,6 +230,7 @@ defmodule ComponentsElixir.Inventory do {key, value}, acc when is_binary(key) -> atom_key = String.to_atom(key) Map.put(acc, atom_key, value) + {key, value}, acc -> Map.put(acc, key, value) end) @@ -281,28 +307,33 @@ defmodule ComponentsElixir.Inventory do {:search, search_term}, query when is_binary(search_term) and search_term != "" -> search_pattern = "%#{search_term}%" - where(query, [c], + + where( + query, + [c], ilike(c.name, ^search_pattern) or - ilike(c.description, ^search_pattern) or - ilike(c.keywords, ^search_pattern) or - ilike(c.position, ^search_pattern) + ilike(c.description, ^search_pattern) or + ilike(c.keywords, ^search_pattern) or + ilike(c.position, ^search_pattern) ) - _, query -> query + _, query -> + query end) end defp apply_component_sorting(query, opts) do case Keyword.get(opts, :sort_criteria, "name_asc") do - "name_asc" -> order_by(query, [c], [asc: c.name]) - "name_desc" -> order_by(query, [c], [desc: c.name]) - "inserted_at_asc" -> order_by(query, [c], [asc: c.inserted_at]) - "inserted_at_desc" -> order_by(query, [c], [desc: c.inserted_at]) - "updated_at_asc" -> order_by(query, [c], [asc: c.updated_at]) - "updated_at_desc" -> order_by(query, [c], [desc: c.updated_at]) - "count_asc" -> order_by(query, [c], [asc: c.count]) - "count_desc" -> order_by(query, [c], [desc: c.count]) - _ -> order_by(query, [c], [asc: c.name]) # Default fallback + "name_asc" -> order_by(query, [c], asc: c.name) + "name_desc" -> order_by(query, [c], desc: c.name) + "inserted_at_asc" -> order_by(query, [c], asc: c.inserted_at) + "inserted_at_desc" -> order_by(query, [c], desc: c.inserted_at) + "updated_at_asc" -> order_by(query, [c], asc: c.updated_at) + "updated_at_desc" -> order_by(query, [c], desc: c.updated_at) + "count_asc" -> order_by(query, [c], asc: c.count) + "count_desc" -> order_by(query, [c], desc: c.count) + # Default fallback + _ -> order_by(query, [c], asc: c.name) end end @@ -353,10 +384,12 @@ defmodule ComponentsElixir.Inventory do def get_inventory_stats do total_components = Repo.aggregate(Component, :count, :id) - total_stock = Component + total_stock = + Component |> Repo.aggregate(:sum, :count) - categories_with_components = Component + categories_with_components = + Component |> distinct([c], c.category_id) |> Repo.aggregate(:count, :category_id) @@ -406,6 +439,7 @@ defmodule ComponentsElixir.Inventory do """ def decrement_component_count(%Component{} = component) do new_count = max(0, component.count - 1) + component |> Component.changeset(%{count: new_count}) |> Repo.update() diff --git a/lib/components_elixir/inventory/storage_location.ex b/lib/components_elixir/inventory/storage_location.ex index 9e2d0d8..8e49233 100644 --- a/lib/components_elixir/inventory/storage_location.ex +++ b/lib/components_elixir/inventory/storage_location.ex @@ -15,7 +15,6 @@ defmodule ComponentsElixir.Inventory.StorageLocation do field :name, :string field :description, :string field :apriltag_id, :integer - field :is_active, :boolean, default: true # Computed/virtual fields - not stored in database field :level, :integer, virtual: true @@ -32,7 +31,7 @@ defmodule ComponentsElixir.Inventory.StorageLocation do @doc false def changeset(storage_location, attrs) do storage_location - |> cast(attrs, [:name, :description, :parent_id, :is_active, :apriltag_id]) + |> cast(attrs, [:name, :description, :parent_id, :apriltag_id]) |> validate_required([:name]) |> validate_length(:name, min: 1, max: 100) |> validate_length(:description, max: 500) diff --git a/lib/components_elixir_web/live/components_live.ex b/lib/components_elixir_web/live/components_live.ex index db2ad05..d36cf8a 100644 --- a/lib/components_elixir_web/live/components_live.ex +++ b/lib/components_elixir_web/live/components_live.ex @@ -97,6 +97,7 @@ defmodule ComponentsElixirWeb.ComponentsLive do def handle_event("category_filter", %{"category_id" => category_id}, socket) do category_id = String.to_integer(category_id) + {:noreply, socket |> assign(:selected_category, category_id) @@ -119,20 +120,26 @@ defmodule ComponentsElixirWeb.ComponentsLive do case Inventory.increment_component_count(component) do {:ok, _updated_component} -> # Only apply sort freeze for dynamic sorting criteria - should_freeze = socket.assigns.sort_criteria in ["count_asc", "count_desc", "updated_at_asc", "updated_at_desc"] - + should_freeze = + socket.assigns.sort_criteria in [ + "count_asc", + "count_desc", + "updated_at_asc", + "updated_at_desc" + ] + if should_freeze do # Cancel any existing timer if socket.assigns.sort_freeze_timer do Process.cancel_timer(socket.assigns.sort_freeze_timer) end - + # Set sort freeze for 3 seconds and mark component as interacting freeze_until = DateTime.add(DateTime.utc_now(), 3, :second) - + # Set new timer to clear interaction state timer_ref = Process.send_after(self(), {:clear_interaction, id}, 3000) - + {:noreply, socket |> put_flash(:info, "Count updated") @@ -160,20 +167,26 @@ defmodule ComponentsElixirWeb.ComponentsLive do case Inventory.decrement_component_count(component) do {:ok, _updated_component} -> # Only apply sort freeze for dynamic sorting criteria - should_freeze = socket.assigns.sort_criteria in ["count_asc", "count_desc", "updated_at_asc", "updated_at_desc"] - + should_freeze = + socket.assigns.sort_criteria in [ + "count_asc", + "count_desc", + "updated_at_asc", + "updated_at_desc" + ] + if should_freeze do # Cancel any existing timer if socket.assigns.sort_freeze_timer do Process.cancel_timer(socket.assigns.sort_freeze_timer) end - + # Set sort freeze for 3 seconds and mark component as interacting freeze_until = DateTime.add(DateTime.utc_now(), 3, :second) - + # Set new timer to clear interaction state timer_ref = Process.send_after(self(), {:clear_interaction, id}, 3000) - + {:noreply, socket |> put_flash(:info, "Count updated") @@ -269,9 +282,11 @@ defmodule ComponentsElixirWeb.ComponentsLive do new_focused_id = if socket.assigns.focused_component_id == component_id do - nil # Unfocus if clicking on the same component + # Unfocus if clicking on the same component + nil else - component_id # Focus on the new component + # Focus on the new component + component_id end {:noreply, assign(socket, :focused_component_id, new_focused_id)} @@ -349,23 +364,26 @@ defmodule ComponentsElixirWeb.ComponentsLive do # Check if sorting should be frozen now = DateTime.utc_now() - should_reload = is_nil(socket.assigns.sort_freeze_until) || - DateTime.compare(now, socket.assigns.sort_freeze_until) != :lt + + should_reload = + is_nil(socket.assigns.sort_freeze_until) || + DateTime.compare(now, socket.assigns.sort_freeze_until) != :lt if should_reload do # Normal loading - query database with current sort criteria - filters = [ - search: socket.assigns.search, - sort_criteria: socket.assigns.sort_criteria, - category_id: socket.assigns.selected_category, - limit: @items_per_page, - offset: socket.assigns.offset - ] - |> Enum.reject(fn - {_, v} when is_nil(v) -> true - {:search, v} when v == "" -> true - {_, _} -> false - end) + filters = + [ + search: socket.assigns.search, + sort_criteria: socket.assigns.sort_criteria, + category_id: socket.assigns.selected_category, + limit: @items_per_page, + offset: socket.assigns.offset + ] + |> Enum.reject(fn + {_, v} when is_nil(v) -> true + {:search, v} when v == "" -> true + {_, _} -> false + end) %{components: new_components, has_more: has_more} = Inventory.paginate_components(filters) @@ -383,7 +401,7 @@ defmodule ComponentsElixirWeb.ComponentsLive do else # Frozen - just update the specific component in place without reordering if socket.assigns.interacting_with do - updated_components = + updated_components = Enum.map(socket.assigns.components, fn component -> if to_string(component.id) == socket.assigns.interacting_with do # Reload this specific component to get updated count @@ -392,7 +410,7 @@ defmodule ComponentsElixirWeb.ComponentsLive do component end end) - + assign(socket, :components, updated_components) else socket @@ -485,8 +503,8 @@ defmodule ComponentsElixirWeb.ComponentsLive do - - + +
JPG, PNG, GIF up to 5MB
<%= for err <- upload_errors(@uploads.image) do %> -<%= Phoenix.Naming.humanize(err) %>
+{Phoenix.Naming.humanize(err)}
<% end %> <%= for entry <- @uploads.image.entries do %> @@ -645,17 +674,23 @@ defmodule ComponentsElixirWeb.ComponentsLive do <.live_img_preview entry={entry} class="h-10 w-10 rounded-lg object-cover" /><%= entry.client_name %>
-<%= entry.progress %>%
+{entry.client_name}
+{entry.progress}%