refactor(elixir): hierarchical refactor

to extract common code patterns from
category/storage location systems
This commit is contained in:
Schuwi
2025-09-17 23:56:56 +02:00
parent 963c9a3770
commit 264adbfb98
12 changed files with 415 additions and 1173 deletions

View File

@@ -2,7 +2,7 @@ defmodule ComponentsElixirWeb.CategoriesLive do
use ComponentsElixirWeb, :live_view
alias ComponentsElixir.{Inventory, Auth}
alias ComponentsElixir.Inventory.Category
alias ComponentsElixir.Inventory.{Category, Hierarchical}
@impl true
def mount(_params, session, socket) do
@@ -121,45 +121,20 @@ defmodule ComponentsElixirWeb.CategoriesLive do
end
defp parent_category_options(categories, editing_category_id \\ nil) do
available_categories =
categories
|> Enum.reject(fn cat ->
cat.id == editing_category_id ||
(editing_category_id && is_descendant?(categories, cat.id, editing_category_id))
end)
|> Enum.map(fn category ->
{category_display_name(category), category.id}
end)
[{"No parent (Root category)", nil}] ++ available_categories
end
defp is_descendant?(categories, descendant_id, ancestor_id) do
# Check if descendant_id is a descendant of ancestor_id
descendant = Enum.find(categories, fn cat -> cat.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?(categories, parent_id, ancestor_id)
end
end
defp category_display_name(category) do
if category.parent do
"#{category.parent.name} > #{category.name}"
else
category.name
end
Hierarchical.parent_select_options(
categories,
editing_category_id,
&(&1.parent),
"No parent (Root category)"
)
end
defp root_categories(categories) do
Enum.filter(categories, fn cat -> is_nil(cat.parent_id) end)
Hierarchical.root_entities(categories, &(&1.parent_id))
end
defp child_categories(categories, parent_id) do
Enum.filter(categories, fn cat -> cat.parent_id == parent_id end)
Hierarchical.child_entities(categories, parent_id, &(&1.parent_id))
end
defp count_components_in_category(category_id) do

View File

@@ -2,7 +2,7 @@ defmodule ComponentsElixirWeb.ComponentsLive do
use ComponentsElixirWeb, :live_view
alias ComponentsElixir.{Inventory, Auth}
alias ComponentsElixir.Inventory.Component
alias ComponentsElixir.Inventory.{Component, Category, StorageLocation, Hierarchical}
@items_per_page 20
@@ -430,31 +430,15 @@ defmodule ComponentsElixirWeb.ComponentsLive do
end
defp category_options(categories) do
[{"Select a category", nil}] ++
Enum.map(categories, fn category ->
{category.name, category.id}
end)
Hierarchical.select_options(categories, &(&1.parent), "Select a category")
end
defp storage_location_display_name(location) do
# Use the computed path from Inventory context for full hierarchy, or fall back to location.path
path = Inventory.compute_storage_location_path(location) || location.path
if path do
# Convert path from "Shelf A/Drawer 2/Box 1" to "Shelf A > Drawer 2 > Box 1"
path
|> String.split("/")
|> Enum.join(" > ")
else
location.name
end
StorageLocation.full_path(location)
end
defp storage_location_options(storage_locations) do
[{"No storage location", nil}] ++
Enum.map(storage_locations, fn location ->
{storage_location_display_name(location), location.id}
end)
Hierarchical.select_options(storage_locations, &(&1.parent), "No storage location")
end
@impl true
@@ -503,7 +487,7 @@ defmodule ComponentsElixirWeb.ComponentsLive do
</div>
</div>
</div>
<!-- Filters -->
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-6">
<div class="flex flex-col sm:flex-row gap-4">
@@ -525,13 +509,9 @@ defmodule ComponentsElixirWeb.ComponentsLive do
class="block w-full px-3 py-2 border border-base-300 rounded-md shadow-sm bg-base-100 text-base-content focus:outline-none focus:ring-primary focus:border-primary sm:text-sm"
>
<option value="" selected={is_nil(@selected_category)}>All Categories</option>
<%= for category <- @categories do %>
<option value={category.id} selected={@selected_category == category.id}>
<%= if category.parent do %>
{category.parent.name} > {category.name}
<% else %>
{category.name}
<% end %>
<%= for {category_name, category_id} <- Hierarchical.select_options(@categories, &(&1.parent)) do %>
<option value={category_id} selected={@selected_category == category_id}>
{category_name}
</option>
<% end %>
</select>
@@ -580,7 +560,7 @@ defmodule ComponentsElixirWeb.ComponentsLive do
</div>
</div>
</div>
<!-- Add Component Modal -->
<%= if @show_add_form do %>
<div class="fixed inset-0 bg-base-content/50 overflow-y-auto h-full w-full z-50">
@@ -714,7 +694,7 @@ defmodule ComponentsElixirWeb.ComponentsLive do
</div>
</div>
<% end %>
<!-- Edit Component Modal -->
<%= if @show_edit_form do %>
<div class="fixed inset-0 bg-base-content/50 overflow-y-auto h-full w-full z-50">
@@ -858,7 +838,7 @@ defmodule ComponentsElixirWeb.ComponentsLive do
</div>
</div>
<% end %>
<!-- Components List -->
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 pb-6">
<div class="bg-base-100 shadow overflow-hidden sm:rounded-md">
@@ -918,7 +898,7 @@ defmodule ComponentsElixirWeb.ComponentsLive do
</button>
</div>
</div>
<!-- Content area with image and details -->
<div class="flex gap-6">
<!-- Large Image -->
@@ -944,7 +924,7 @@ defmodule ComponentsElixirWeb.ComponentsLive do
</div>
<% end %>
</div>
<!-- Details -->
<div class="flex-1 space-y-4 select-text">
<!-- Full Description -->
@@ -956,7 +936,7 @@ defmodule ComponentsElixirWeb.ComponentsLive do
</p>
</div>
<% end %>
<!-- Metadata Grid -->
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 text-sm">
<%= if component.storage_location do %>
@@ -1013,7 +993,7 @@ defmodule ComponentsElixirWeb.ComponentsLive do
</div>
</div>
</div>
<!-- Action Buttons -->
<div class="flex justify-end items-center space-x-2 pt-4 border-t border-base-300">
<button
@@ -1098,7 +1078,7 @@ defmodule ComponentsElixirWeb.ComponentsLive do
</span>
</div>
</div>
<!-- Middle row: Description -->
<%= if component.description do %>
<div class="mt-1">
@@ -1107,7 +1087,7 @@ defmodule ComponentsElixirWeb.ComponentsLive do
</p>
</div>
<% end %>
<!-- Bottom row: Metadata -->
<div class="mt-2 grid grid-cols-1 sm:grid-cols-3 gap-x-4 gap-y-1 text-sm text-base-content/60">
<%= if component.storage_location do %>
@@ -1135,7 +1115,7 @@ defmodule ComponentsElixirWeb.ComponentsLive do
</div>
<% end %>
</div>
<!-- Keywords row -->
<%= if component.keywords do %>
<div class="mt-2">
@@ -1218,7 +1198,7 @@ defmodule ComponentsElixirWeb.ComponentsLive do
>
<!-- Background overlay -->
<div class="absolute inset-0 bg-black bg-opacity-75"></div>
<!-- Modal content -->
<div
class="relative bg-base-100 rounded-lg shadow-xl max-w-4xl w-full max-h-full overflow-auto"
@@ -1236,7 +1216,7 @@ defmodule ComponentsElixirWeb.ComponentsLive do
×
</button>
</div>
<!-- Content -->
<div class="p-6 bg-base-100 rounded-b-lg">
<div class="text-center">

View File

@@ -5,7 +5,7 @@ defmodule ComponentsElixirWeb.StorageLocationsLive do
use ComponentsElixirWeb, :live_view
alias ComponentsElixir.{Inventory, Auth}
alias ComponentsElixir.Inventory.StorageLocation
alias ComponentsElixir.Inventory.{StorageLocation, Hierarchical}
alias ComponentsElixir.AprilTag
@impl true
@@ -240,48 +240,24 @@ defmodule ComponentsElixirWeb.StorageLocationsLive do
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
Hierarchical.parent_select_options(
storage_locations,
editing_location_id,
&(&1.parent),
"No parent (Root location)"
)
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
StorageLocation.full_path(location)
end
defp root_storage_locations(storage_locations) do
Enum.filter(storage_locations, fn loc -> is_nil(loc.parent_id) end)
Hierarchical.root_entities(storage_locations, &(&1.parent_id))
end
defp child_storage_locations(storage_locations, parent_id) do
Enum.filter(storage_locations, fn loc -> loc.parent_id == parent_id end)
Hierarchical.child_entities(storage_locations, parent_id, &(&1.parent_id))
end
defp count_components_in_location(location_id) do
@@ -766,7 +742,7 @@ defmodule ComponentsElixirWeb.StorageLocationsLive do
<span class="text-sm text-base-content/70 ml-2">(AprilTag ID {scan.apriltag_id})</span>
</div>
<span class="text-xs text-green-600 bg-green-100 px-2 py-1 rounded">
Level {scan.location.level}
Level <%= Hierarchical.compute_level(scan.location, &(&1.parent)) %>
</span>
</div>
</div>