style: format codebase
This commit is contained in:
@@ -195,7 +195,10 @@ defmodule ComponentsElixirWeb.CoreComponents do
|
||||
name={@name}
|
||||
value="true"
|
||||
checked={@checked}
|
||||
class={@class || "checkbox checkbox-sm border-base-300 checked:bg-primary checked:border-primary"}
|
||||
class={
|
||||
@class ||
|
||||
"checkbox checkbox-sm border-base-300 checked:bg-primary checked:border-primary"
|
||||
}
|
||||
{@rest}
|
||||
/>{@label}
|
||||
</span>
|
||||
@@ -213,7 +216,10 @@ defmodule ComponentsElixirWeb.CoreComponents do
|
||||
<select
|
||||
id={@id}
|
||||
name={@name}
|
||||
class={[@class || "w-full select bg-base-100 border-base-300 text-base-content", @errors != [] && (@error_class || "select-error border-error")]}
|
||||
class={[
|
||||
@class || "w-full select bg-base-100 border-base-300 text-base-content",
|
||||
@errors != [] && (@error_class || "select-error border-error")
|
||||
]}
|
||||
multiple={@multiple}
|
||||
{@rest}
|
||||
>
|
||||
@@ -235,7 +241,8 @@ defmodule ComponentsElixirWeb.CoreComponents do
|
||||
id={@id}
|
||||
name={@name}
|
||||
class={[
|
||||
@class || "w-full textarea bg-base-100 border-base-300 text-base-content placeholder:text-base-content/50",
|
||||
@class ||
|
||||
"w-full textarea bg-base-100 border-base-300 text-base-content placeholder:text-base-content/50",
|
||||
@errors != [] && (@error_class || "textarea-error border-error")
|
||||
]}
|
||||
{@rest}
|
||||
@@ -258,7 +265,8 @@ defmodule ComponentsElixirWeb.CoreComponents do
|
||||
id={@id}
|
||||
value={Phoenix.HTML.Form.normalize_value(@type, @value)}
|
||||
class={[
|
||||
@class || "w-full input bg-base-100 border-base-300 text-base-content placeholder:text-base-content/50",
|
||||
@class ||
|
||||
"w-full input bg-base-100 border-base-300 text-base-content placeholder:text-base-content/50",
|
||||
@errors != [] && (@error_class || "input-error border-error")
|
||||
]}
|
||||
{@rest}
|
||||
|
||||
@@ -13,7 +13,8 @@ defmodule ComponentsElixirWeb.FileController do
|
||||
|
||||
conn
|
||||
|> put_resp_content_type(mime_type)
|
||||
|> put_resp_header("cache-control", "public, max-age=86400") # Cache for 1 day
|
||||
# Cache for 1 day
|
||||
|> put_resp_header("cache-control", "public, max-age=86400")
|
||||
|> send_file(200, file_path)
|
||||
else
|
||||
conn
|
||||
@@ -40,7 +41,8 @@ defmodule ComponentsElixirWeb.FileController do
|
||||
|
||||
conn
|
||||
|> put_resp_content_type(mime_type)
|
||||
|> put_resp_header("cache-control", "public, max-age=86400") # Cache for 1 day
|
||||
# Cache for 1 day
|
||||
|> put_resp_header("cache-control", "public, max-age=86400")
|
||||
|> put_resp_header("content-disposition", "inline; filename=\"#{filename}\"")
|
||||
|> send_file(200, file_path)
|
||||
else
|
||||
@@ -64,9 +66,9 @@ defmodule ComponentsElixirWeb.FileController do
|
||||
# Security validation: prevent directory traversal and only allow safe characters
|
||||
# Allow letters, numbers, spaces, dots, dashes, underscores, parentheses, and basic punctuation
|
||||
if String.match?(decoded_filename, ~r/^[a-zA-Z0-9\s_\-\.\(\)\[\]]+$/) and
|
||||
not String.contains?(decoded_filename, "..") and
|
||||
not String.starts_with?(decoded_filename, "/") and
|
||||
not String.contains?(decoded_filename, "\\") do
|
||||
not String.contains?(decoded_filename, "..") and
|
||||
not String.starts_with?(decoded_filename, "/") and
|
||||
not String.contains?(decoded_filename, "\\") do
|
||||
{:ok, decoded_filename}
|
||||
else
|
||||
{:error, "Invalid filename: contains unsafe characters"}
|
||||
|
||||
@@ -46,12 +46,13 @@ defmodule ComponentsElixirWeb.CategoriesLive do
|
||||
def handle_event("show_edit_form", %{"id" => id}, socket) do
|
||||
category = Inventory.get_category!(id)
|
||||
# Create a changeset with current values forced into changes for proper form display
|
||||
changeset = Inventory.change_category(category, %{
|
||||
name: category.name,
|
||||
description: category.description,
|
||||
parent_id: category.parent_id
|
||||
})
|
||||
|> Ecto.Changeset.force_change(:parent_id, category.parent_id)
|
||||
changeset =
|
||||
Inventory.change_category(category, %{
|
||||
name: category.name,
|
||||
description: category.description,
|
||||
parent_id: category.parent_id
|
||||
})
|
||||
|> Ecto.Changeset.force_change(:parent_id, category.parent_id)
|
||||
|
||||
form = to_form(changeset)
|
||||
|
||||
@@ -112,7 +113,12 @@ defmodule ComponentsElixirWeb.CategoriesLive do
|
||||
|> reload_categories()}
|
||||
|
||||
{:error, _changeset} ->
|
||||
{:noreply, put_flash(socket, :error, "Cannot delete category - it may have components assigned or child categories")}
|
||||
{:noreply,
|
||||
put_flash(
|
||||
socket,
|
||||
:error,
|
||||
"Cannot delete category - it may have components assigned or child categories"
|
||||
)}
|
||||
end
|
||||
end
|
||||
|
||||
@@ -120,11 +126,12 @@ defmodule ComponentsElixirWeb.CategoriesLive do
|
||||
category_id = String.to_integer(id)
|
||||
expanded_categories = socket.assigns.expanded_categories
|
||||
|
||||
new_expanded = if MapSet.member?(expanded_categories, category_id) do
|
||||
MapSet.delete(expanded_categories, category_id)
|
||||
else
|
||||
MapSet.put(expanded_categories, category_id)
|
||||
end
|
||||
new_expanded =
|
||||
if MapSet.member?(expanded_categories, category_id) do
|
||||
MapSet.delete(expanded_categories, category_id)
|
||||
else
|
||||
MapSet.put(expanded_categories, category_id)
|
||||
end
|
||||
|
||||
{:noreply, assign(socket, :expanded_categories, new_expanded)}
|
||||
end
|
||||
@@ -138,17 +145,17 @@ defmodule ComponentsElixirWeb.CategoriesLive do
|
||||
Hierarchical.parent_select_options(
|
||||
categories,
|
||||
editing_category_id,
|
||||
&(&1.parent),
|
||||
& &1.parent,
|
||||
"No parent (Root category)"
|
||||
)
|
||||
end
|
||||
|
||||
defp root_categories(categories) do
|
||||
Hierarchical.root_entities(categories, &(&1.parent_id))
|
||||
Hierarchical.root_entities(categories, & &1.parent_id)
|
||||
end
|
||||
|
||||
defp child_categories(categories, parent_id) do
|
||||
Hierarchical.child_entities(categories, parent_id, &(&1.parent_id))
|
||||
Hierarchical.child_entities(categories, parent_id, & &1.parent_id)
|
||||
end
|
||||
|
||||
defp count_components_in_category(category_id) do
|
||||
@@ -164,38 +171,41 @@ defmodule ComponentsElixirWeb.CategoriesLive do
|
||||
border_class = if assigns.depth > 0, do: "border-l-2 border-base-300 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
|
||||
{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
|
||||
|
||||
children = child_categories(assigns.categories, assigns.category.id)
|
||||
has_children = !Enum.empty?(children)
|
||||
is_expanded = MapSet.member?(assigns.expanded_categories, assigns.category.id)
|
||||
|
||||
# Calculate component counts including descendants
|
||||
{self_count, children_count, _total_count} = Hierarchical.count_with_descendants(
|
||||
assigns.category.id,
|
||||
assigns.categories,
|
||||
&(&1.parent_id),
|
||||
&count_components_in_category/1
|
||||
)
|
||||
{self_count, children_count, _total_count} =
|
||||
Hierarchical.count_with_descendants(
|
||||
assigns.category.id,
|
||||
assigns.categories,
|
||||
& &1.parent_id,
|
||||
&count_components_in_category/1
|
||||
)
|
||||
|
||||
# Format count display
|
||||
count_display = Hierarchical.format_count_display(self_count, children_count, is_expanded)
|
||||
|
||||
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(:children, children)
|
||||
|> assign(:has_children, has_children)
|
||||
|> assign(:is_expanded, is_expanded)
|
||||
|> assign(:count_display, count_display)
|
||||
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(:children, children)
|
||||
|> assign(:has_children, has_children)
|
||||
|> assign(:is_expanded, is_expanded)
|
||||
|> assign(:count_display, count_display)
|
||||
|
||||
~H"""
|
||||
<div class={[@border_class, @depth > 0 && "mt-4"]} style={"margin-left: #{@margin_left}px"}>
|
||||
@@ -215,12 +225,16 @@ defmodule ComponentsElixirWeb.CategoriesLive do
|
||||
<% end %>
|
||||
</button>
|
||||
<% else %>
|
||||
<div class="w-6"></div> <!-- Spacer for alignment -->
|
||||
<div class="w-6"></div>
|
||||
<!-- Spacer for alignment -->
|
||||
<% end %>
|
||||
|
||||
<.icon name="hero-folder" class={"#{@icon_size} #{if @depth == 0, do: "text-primary", else: "text-base-content/60"} mt-0.5"} />
|
||||
|
||||
<!-- Content area - always starts at same vertical position -->
|
||||
<.icon
|
||||
name="hero-folder"
|
||||
class={"#{@icon_size} #{if @depth == 0, do: "text-primary", else: "text-base-content/60"} mt-0.5"}
|
||||
/>
|
||||
|
||||
<!-- Content area - always starts at same vertical position -->
|
||||
<div class="flex-1">
|
||||
<!-- Minimized view (default) -->
|
||||
<%= unless @is_expanded do %>
|
||||
@@ -268,8 +282,8 @@ defmodule ComponentsElixirWeb.CategoriesLive do
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<!-- Expanded view -->
|
||||
|
||||
<!-- Expanded view -->
|
||||
<%= if @is_expanded do %>
|
||||
<div class="flex items-start justify-between">
|
||||
<div class="flex-1">
|
||||
@@ -321,11 +335,16 @@ defmodule ComponentsElixirWeb.CategoriesLive do
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Render children recursively (only when expanded) -->
|
||||
|
||||
<!-- Render children recursively (only when expanded) -->
|
||||
<%= if @is_expanded do %>
|
||||
<%= for child <- @children do %>
|
||||
<.category_item category={child} categories={@categories} expanded_categories={@expanded_categories} depth={@depth + 1} />
|
||||
<.category_item
|
||||
category={child}
|
||||
categories={@categories}
|
||||
expanded_categories={@expanded_categories}
|
||||
depth={@depth + 1}
|
||||
/>
|
||||
<% end %>
|
||||
<% end %>
|
||||
</div>
|
||||
@@ -368,8 +387,8 @@ defmodule ComponentsElixirWeb.CategoriesLive do
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Add Category Modal -->
|
||||
|
||||
<!-- Add Category Modal -->
|
||||
<%= if @show_add_form do %>
|
||||
<div class="fixed inset-0 bg-base-content/50 overflow-y-auto h-full w-full z-50">
|
||||
<div class="relative top-20 mx-auto p-5 border border-base-300 w-11/12 md:w-1/2 shadow-lg rounded-md bg-base-100">
|
||||
@@ -424,8 +443,8 @@ defmodule ComponentsElixirWeb.CategoriesLive do
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<!-- Edit Category Modal -->
|
||||
|
||||
<!-- Edit Category Modal -->
|
||||
<%= if @show_edit_form do %>
|
||||
<div class="fixed inset-0 bg-base-content/50 overflow-y-auto h-full w-full z-50">
|
||||
<div class="relative top-20 mx-auto p-5 border border-base-300 w-11/12 md:w-1/2 shadow-lg rounded-md bg-base-100">
|
||||
@@ -480,13 +499,15 @@ defmodule ComponentsElixirWeb.CategoriesLive do
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<!-- Categories List -->
|
||||
|
||||
<!-- Categories List -->
|
||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-6">
|
||||
<div class="bg-base-100 shadow overflow-hidden sm:rounded-md">
|
||||
<div class="px-6 py-4 border-b border-base-300">
|
||||
<h2 class="text-lg font-medium text-base-content">Category Hierarchy</h2>
|
||||
<p class="text-sm text-base-content/60 mt-1">Manage your component categories and subcategories</p>
|
||||
<p class="text-sm text-base-content/60 mt-1">
|
||||
Manage your component categories and subcategories
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<%= if Enum.empty?(@categories) do %>
|
||||
@@ -501,8 +522,7 @@ defmodule ComponentsElixirWeb.CategoriesLive do
|
||||
phx-click="show_add_form"
|
||||
class="inline-flex items-center px-4 py-2 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700"
|
||||
>
|
||||
<.icon name="hero-plus" class="w-4 h-4 mr-2" />
|
||||
Add Category
|
||||
<.icon name="hero-plus" class="w-4 h-4 mr-2" /> Add Category
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -511,7 +531,12 @@ defmodule ComponentsElixirWeb.CategoriesLive do
|
||||
<!-- Recursive Category Tree -->
|
||||
<%= for category <- root_categories(@categories) do %>
|
||||
<div class="px-6 py-4">
|
||||
<.category_item category={category} categories={@categories} expanded_categories={@expanded_categories} depth={0} />
|
||||
<.category_item
|
||||
category={category}
|
||||
categories={@categories}
|
||||
expanded_categories={@expanded_categories}
|
||||
depth={0}
|
||||
/>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
@@ -139,7 +139,11 @@ defmodule ComponentsElixirWeb.ComponentsLive do
|
||||
|> push_patch(to: path)}
|
||||
end
|
||||
|
||||
def handle_event("storage_location_filter", %{"storage_location_id" => storage_location_id}, socket) do
|
||||
def handle_event(
|
||||
"storage_location_filter",
|
||||
%{"storage_location_id" => storage_location_id},
|
||||
socket
|
||||
) do
|
||||
storage_location_id = String.to_integer(storage_location_id)
|
||||
query_string = build_query_params_with_storage_location(socket, storage_location_id)
|
||||
path = if query_string == "", do: "/", else: "/?" <> query_string
|
||||
@@ -387,7 +391,10 @@ defmodule ComponentsElixirWeb.ComponentsLive do
|
||||
|> save_uploaded_image(component_params)
|
||||
|> save_uploaded_datasheet(socket)
|
||||
|
||||
case Inventory.update_component_with_datasheet(socket.assigns.editing_component, updated_params) do
|
||||
case Inventory.update_component_with_datasheet(
|
||||
socket.assigns.editing_component,
|
||||
updated_params
|
||||
) do
|
||||
{:ok, _component} ->
|
||||
{:noreply,
|
||||
socket
|
||||
@@ -496,7 +503,8 @@ defmodule ComponentsElixirWeb.ComponentsLive do
|
||||
search: Map.get(overrides, :search, socket.assigns.search),
|
||||
criteria: Map.get(overrides, :criteria, socket.assigns.sort_criteria),
|
||||
category_id: Map.get(overrides, :category_id, socket.assigns.selected_category),
|
||||
storage_location_id: Map.get(overrides, :storage_location_id, socket.assigns.selected_storage_location)
|
||||
storage_location_id:
|
||||
Map.get(overrides, :storage_location_id, socket.assigns.selected_storage_location)
|
||||
}
|
||||
|
||||
params
|
||||
@@ -506,12 +514,14 @@ defmodule ComponentsElixirWeb.ComponentsLive do
|
||||
|
||||
defp parse_filter_id(nil), do: nil
|
||||
defp parse_filter_id(""), do: nil
|
||||
|
||||
defp parse_filter_id(id) when is_binary(id) do
|
||||
case Integer.parse(id) do
|
||||
{int_id, ""} -> int_id
|
||||
_ -> nil
|
||||
end
|
||||
end
|
||||
|
||||
defp parse_filter_id(id) when is_integer(id), do: id
|
||||
|
||||
defp build_query_params_with_category(socket, category_id) do
|
||||
@@ -553,7 +563,7 @@ defmodule ComponentsElixirWeb.ComponentsLive do
|
||||
end
|
||||
|
||||
defp category_options(categories) do
|
||||
Hierarchical.select_options(categories, &(&1.parent), "Select a category")
|
||||
Hierarchical.select_options(categories, & &1.parent, "Select a category")
|
||||
end
|
||||
|
||||
defp storage_location_display_name(location) do
|
||||
@@ -561,7 +571,7 @@ defmodule ComponentsElixirWeb.ComponentsLive do
|
||||
end
|
||||
|
||||
defp storage_location_options(storage_locations) do
|
||||
Hierarchical.select_options(storage_locations, &(&1.parent), "No storage location")
|
||||
Hierarchical.select_options(storage_locations, & &1.parent, "No storage location")
|
||||
end
|
||||
|
||||
@impl true
|
||||
@@ -610,7 +620,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">
|
||||
@@ -687,23 +697,27 @@ defmodule ComponentsElixirWeb.ComponentsLive do
|
||||
class="inline-flex items-center px-3 py-2 border border-base-300 text-sm font-medium rounded-md text-base-content bg-base-100 hover:bg-base-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary"
|
||||
>
|
||||
<.icon name="hero-adjustments-horizontal" class="w-4 h-4 mr-2" />
|
||||
<%= if @show_advanced_filters, do: "Hide", else: "More" %> Filters
|
||||
{if @show_advanced_filters, do: "Hide", else: "More"} Filters
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Advanced Filters (Collapsible) -->
|
||||
|
||||
<!-- Advanced Filters (Collapsible) -->
|
||||
<%= if @show_advanced_filters do %>
|
||||
<div class="mt-4 p-4 bg-base-100 border border-base-300 rounded-md">
|
||||
<div class="flex flex-col sm:flex-row gap-4">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-base-content mb-2">Storage Location</label>
|
||||
<label class="block text-sm font-medium text-base-content mb-2">
|
||||
Storage Location
|
||||
</label>
|
||||
<form phx-change="storage_location_filter">
|
||||
<select
|
||||
name="storage_location_id"
|
||||
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_storage_location)}>All Storage Locations</option>
|
||||
<option value="" selected={is_nil(@selected_storage_location)}>
|
||||
All Storage Locations
|
||||
</option>
|
||||
<%= for {location_name, location_id} <- Hierarchical.select_options(@storage_locations, &(&1.parent)) do %>
|
||||
<option value={location_id} selected={@selected_storage_location == location_id}>
|
||||
{location_name}
|
||||
@@ -716,7 +730,7 @@ defmodule ComponentsElixirWeb.ComponentsLive do
|
||||
</div>
|
||||
<% end %>
|
||||
</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">
|
||||
@@ -895,7 +909,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">
|
||||
@@ -1029,8 +1043,7 @@ defmodule ComponentsElixirWeb.ComponentsLive do
|
||||
target="_blank"
|
||||
class="inline-flex items-center text-primary hover:text-primary/80"
|
||||
>
|
||||
<.icon name="hero-document-text" class="w-4 h-4 mr-1" />
|
||||
View PDF
|
||||
<.icon name="hero-document-text" class="w-4 h-4 mr-1" /> View PDF
|
||||
</a>
|
||||
</div>
|
||||
<% end %>
|
||||
@@ -1097,7 +1110,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">
|
||||
@@ -1166,7 +1179,7 @@ defmodule ComponentsElixirWeb.ComponentsLive do
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- Content area with image and details -->
|
||||
<div class="flex gap-6">
|
||||
<!-- Large Image -->
|
||||
@@ -1192,7 +1205,7 @@ defmodule ComponentsElixirWeb.ComponentsLive do
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- Details -->
|
||||
<div class="flex-1 space-y-4 select-text">
|
||||
<!-- Full Description -->
|
||||
@@ -1201,10 +1214,13 @@ defmodule ComponentsElixirWeb.ComponentsLive do
|
||||
<h4 class="text-sm font-medium text-base-content mb-2">Description</h4>
|
||||
<%!-- Keep interpolation inline to prevent whitespace-pre-wrap from preserving template indentation.
|
||||
Use phx-no-format so the formatter won't break the layout. --%>
|
||||
<p phx-no-format class="text-sm text-base-content/70 leading-relaxed whitespace-pre-wrap">{component.description}</p>
|
||||
<p
|
||||
phx-no-format
|
||||
class="text-sm text-base-content/70 leading-relaxed whitespace-pre-wrap"
|
||||
>{component.description}</p>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
|
||||
<!-- Metadata Grid -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 text-sm">
|
||||
<%= if component.storage_location do %>
|
||||
@@ -1261,7 +1277,10 @@ defmodule ComponentsElixirWeb.ComponentsLive do
|
||||
|
||||
<%= if component.datasheet_filename || component.datasheet_url do %>
|
||||
<div class="flex items-start">
|
||||
<.icon name="hero-document-text" class="w-4 h-4 mr-2 text-base-content/50 mt-0.5 flex-shrink-0" />
|
||||
<.icon
|
||||
name="hero-document-text"
|
||||
class="w-4 h-4 mr-2 text-base-content/50 mt-0.5 flex-shrink-0"
|
||||
/>
|
||||
<div>
|
||||
<span class="font-medium text-base-content">Datasheet:</span>
|
||||
<div class="space-y-1 mt-1">
|
||||
@@ -1284,8 +1303,7 @@ defmodule ComponentsElixirWeb.ComponentsLive do
|
||||
target="_blank"
|
||||
class="inline-flex items-center text-base-content/70 hover:text-primary text-sm"
|
||||
>
|
||||
<.icon name="hero-link" class="w-4 h-4 mr-1" />
|
||||
Original URL
|
||||
<.icon name="hero-link" class="w-4 h-4 mr-1" /> Original URL
|
||||
</a>
|
||||
</div>
|
||||
<% end %>
|
||||
@@ -1296,7 +1314,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
|
||||
@@ -1390,16 +1408,19 @@ defmodule ComponentsElixirWeb.ComponentsLive do
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- Middle row: Description -->
|
||||
<%= if component.description do %>
|
||||
<div class="mt-1">
|
||||
<%!-- Keep interpolation inline to prevent whitespace-pre-wrap from preserving template indentation.
|
||||
Use phx-no-format so the formatter won't break the layout. --%>
|
||||
<p phx-no-format class="text-sm text-base-content/70 line-clamp-2 whitespace-pre-wrap">{component.description}</p>
|
||||
<p
|
||||
phx-no-format
|
||||
class="text-sm text-base-content/70 line-clamp-2 whitespace-pre-wrap"
|
||||
>{component.description}</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 %>
|
||||
@@ -1427,7 +1448,7 @@ defmodule ComponentsElixirWeb.ComponentsLive do
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- Keywords row -->
|
||||
<%= if component.keywords do %>
|
||||
<div class="mt-2">
|
||||
@@ -1510,7 +1531,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"
|
||||
@@ -1528,7 +1549,7 @@ defmodule ComponentsElixirWeb.ComponentsLive do
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- Content -->
|
||||
<div class="p-6 bg-base-100 rounded-b-lg">
|
||||
<div class="text-center">
|
||||
|
||||
@@ -66,7 +66,7 @@ defmodule ComponentsElixirWeb.LoginLive do
|
||||
|
||||
<%= if @error_message do %>
|
||||
<div class="text-red-600 text-sm text-center">
|
||||
<%= @error_message %>
|
||||
{@error_message}
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
|
||||
@@ -54,12 +54,13 @@ defmodule ComponentsElixirWeb.StorageLocationsLive do
|
||||
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)
|
||||
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)
|
||||
|
||||
@@ -82,29 +83,31 @@ defmodule ComponentsElixirWeb.StorageLocationsLive do
|
||||
|
||||
def handle_event("save_location", %{"storage_location" => location_params}, socket) do
|
||||
# Process AprilTag assignment based on mode
|
||||
processed_params = case socket.assigns.apriltag_mode do
|
||||
"none" ->
|
||||
# Remove any apriltag_id from params to ensure it's nil
|
||||
Map.delete(location_params, "apriltag_id")
|
||||
processed_params =
|
||||
case socket.assigns.apriltag_mode do
|
||||
"none" ->
|
||||
# Remove any apriltag_id from params to ensure it's nil
|
||||
Map.delete(location_params, "apriltag_id")
|
||||
|
||||
"auto" ->
|
||||
# Auto-assign next available AprilTag ID
|
||||
case AprilTag.next_available_apriltag_id() do
|
||||
nil ->
|
||||
# No available IDs, proceed without AprilTag
|
||||
Map.delete(location_params, "apriltag_id")
|
||||
apriltag_id ->
|
||||
Map.put(location_params, "apriltag_id", apriltag_id)
|
||||
end
|
||||
"auto" ->
|
||||
# Auto-assign next available AprilTag ID
|
||||
case AprilTag.next_available_apriltag_id() do
|
||||
nil ->
|
||||
# No available IDs, proceed without AprilTag
|
||||
Map.delete(location_params, "apriltag_id")
|
||||
|
||||
"manual" ->
|
||||
# Use the manually entered apriltag_id (validation will be handled by changeset)
|
||||
location_params
|
||||
apriltag_id ->
|
||||
Map.put(location_params, "apriltag_id", apriltag_id)
|
||||
end
|
||||
|
||||
_ ->
|
||||
# Fallback: remove apriltag_id
|
||||
Map.delete(location_params, "apriltag_id")
|
||||
end
|
||||
"manual" ->
|
||||
# Use the manually entered apriltag_id (validation will be handled by changeset)
|
||||
location_params
|
||||
|
||||
_ ->
|
||||
# Fallback: remove apriltag_id
|
||||
Map.delete(location_params, "apriltag_id")
|
||||
end
|
||||
|
||||
case Inventory.create_storage_location(processed_params) do
|
||||
{:ok, _location} ->
|
||||
@@ -147,7 +150,12 @@ defmodule ComponentsElixirWeb.StorageLocationsLive do
|
||||
|> reload_storage_locations()}
|
||||
|
||||
{:error, _changeset} ->
|
||||
{:noreply, put_flash(socket, :error, "Cannot delete storage location - it may have components assigned or child locations")}
|
||||
{:noreply,
|
||||
put_flash(
|
||||
socket,
|
||||
:error,
|
||||
"Cannot delete storage location - it may have components assigned or child locations"
|
||||
)}
|
||||
end
|
||||
end
|
||||
|
||||
@@ -164,10 +172,17 @@ defmodule ComponentsElixirWeb.StorageLocationsLive do
|
||||
{apriltag_id, ""} when apriltag_id >= 0 and apriltag_id <= 586 ->
|
||||
case Inventory.get_storage_location_by_apriltag_id(apriltag_id) do
|
||||
nil ->
|
||||
{:noreply, put_flash(socket, :error, "Storage location not found for AprilTag ID: #{apriltag_id}")}
|
||||
{:noreply,
|
||||
put_flash(
|
||||
socket,
|
||||
:error,
|
||||
"Storage location not found for AprilTag ID: #{apriltag_id}"
|
||||
)}
|
||||
|
||||
location ->
|
||||
scanned_tags = [%{apriltag_id: apriltag_id, location: location} | socket.assigns.scanned_tags]
|
||||
scanned_tags = [
|
||||
%{apriltag_id: apriltag_id, location: location} | socket.assigns.scanned_tags
|
||||
]
|
||||
|
||||
{:noreply,
|
||||
socket
|
||||
@@ -188,11 +203,12 @@ defmodule ComponentsElixirWeb.StorageLocationsLive do
|
||||
location_id = String.to_integer(id)
|
||||
expanded_locations = socket.assigns.expanded_locations
|
||||
|
||||
new_expanded = if MapSet.member?(expanded_locations, location_id) do
|
||||
MapSet.delete(expanded_locations, location_id)
|
||||
else
|
||||
MapSet.put(expanded_locations, location_id)
|
||||
end
|
||||
new_expanded =
|
||||
if MapSet.member?(expanded_locations, location_id) do
|
||||
MapSet.delete(expanded_locations, location_id)
|
||||
else
|
||||
MapSet.put(expanded_locations, location_id)
|
||||
end
|
||||
|
||||
{:noreply, assign(socket, :expanded_locations, new_expanded)}
|
||||
end
|
||||
@@ -203,19 +219,26 @@ defmodule ComponentsElixirWeb.StorageLocationsLive do
|
||||
|
||||
def handle_event("set_edit_apriltag_mode", %{"mode" => mode}, socket) do
|
||||
# Clear the apriltag_id field when switching modes
|
||||
form = case mode do
|
||||
"remove" ->
|
||||
socket.assigns.form
|
||||
|> Phoenix.Component.to_form()
|
||||
|> Map.put(:params, Map.put(socket.assigns.form.params || %{}, "apriltag_id", nil))
|
||||
"keep" ->
|
||||
current_id = socket.assigns.editing_location.apriltag_id
|
||||
socket.assigns.form
|
||||
|> Phoenix.Component.to_form()
|
||||
|> Map.put(:params, Map.put(socket.assigns.form.params || %{}, "apriltag_id", current_id))
|
||||
_ ->
|
||||
socket.assigns.form
|
||||
end
|
||||
form =
|
||||
case mode do
|
||||
"remove" ->
|
||||
socket.assigns.form
|
||||
|> Phoenix.Component.to_form()
|
||||
|> Map.put(:params, Map.put(socket.assigns.form.params || %{}, "apriltag_id", nil))
|
||||
|
||||
"keep" ->
|
||||
current_id = socket.assigns.editing_location.apriltag_id
|
||||
|
||||
socket.assigns.form
|
||||
|> Phoenix.Component.to_form()
|
||||
|> Map.put(
|
||||
:params,
|
||||
Map.put(socket.assigns.form.params || %{}, "apriltag_id", current_id)
|
||||
)
|
||||
|
||||
_ ->
|
||||
socket.assigns.form
|
||||
end
|
||||
|
||||
{:noreply,
|
||||
socket
|
||||
@@ -234,7 +257,8 @@ defmodule ComponentsElixirWeb.StorageLocationsLive do
|
||||
{:noreply, put_flash(socket, :error, "Failed to get AprilTag URL")}
|
||||
|
||||
apriltag_url ->
|
||||
filename = "#{location.name |> String.replace(" ", "_")}_AprilTag_#{location.apriltag_id}.svg"
|
||||
filename =
|
||||
"#{location.name |> String.replace(" ", "_")}_AprilTag_#{location.apriltag_id}.svg"
|
||||
|
||||
# Send file download to browser
|
||||
{:noreply,
|
||||
@@ -257,7 +281,7 @@ defmodule ComponentsElixirWeb.StorageLocationsLive do
|
||||
Hierarchical.parent_select_options(
|
||||
storage_locations,
|
||||
editing_location_id,
|
||||
&(&1.parent),
|
||||
& &1.parent,
|
||||
"No parent (Root location)"
|
||||
)
|
||||
end
|
||||
@@ -267,11 +291,11 @@ defmodule ComponentsElixirWeb.StorageLocationsLive do
|
||||
end
|
||||
|
||||
defp root_storage_locations(storage_locations) do
|
||||
Hierarchical.root_entities(storage_locations, &(&1.parent_id))
|
||||
Hierarchical.root_entities(storage_locations, & &1.parent_id)
|
||||
end
|
||||
|
||||
defp child_storage_locations(storage_locations, parent_id) do
|
||||
Hierarchical.child_entities(storage_locations, parent_id, &(&1.parent_id))
|
||||
Hierarchical.child_entities(storage_locations, parent_id, & &1.parent_id)
|
||||
end
|
||||
|
||||
defp count_components_in_location(location_id) do
|
||||
@@ -291,46 +315,53 @@ defmodule ComponentsElixirWeb.StorageLocationsLive do
|
||||
border_class = if assigns.depth > 0, do: "border-l-2 border-base-300 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
|
||||
{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
|
||||
icon_name =
|
||||
case assigns.depth do
|
||||
# Shelf/Room
|
||||
0 -> "hero-building-office"
|
||||
# Drawer/Cabinet
|
||||
1 -> "hero-archive-box"
|
||||
# Box/Container
|
||||
_ -> "hero-cube"
|
||||
end
|
||||
|
||||
children = child_storage_locations(assigns.storage_locations, assigns.location.id)
|
||||
has_children = !Enum.empty?(children)
|
||||
is_expanded = MapSet.member?(assigns.expanded_locations, assigns.location.id)
|
||||
|
||||
# Calculate component counts including descendants
|
||||
{self_count, children_count, _total_count} = Hierarchical.count_with_descendants(
|
||||
assigns.location.id,
|
||||
assigns.storage_locations,
|
||||
&(&1.parent_id),
|
||||
&count_components_in_location/1
|
||||
)
|
||||
{self_count, children_count, _total_count} =
|
||||
Hierarchical.count_with_descendants(
|
||||
assigns.location.id,
|
||||
assigns.storage_locations,
|
||||
& &1.parent_id,
|
||||
&count_components_in_location/1
|
||||
)
|
||||
|
||||
# Format count display
|
||||
count_display = Hierarchical.format_count_display(self_count, children_count, is_expanded)
|
||||
|
||||
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, children)
|
||||
|> assign(:has_children, has_children)
|
||||
|> assign(:is_expanded, is_expanded)
|
||||
|> assign(:count_display, count_display)
|
||||
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, children)
|
||||
|> assign(:has_children, has_children)
|
||||
|> assign(:is_expanded, is_expanded)
|
||||
|> assign(:count_display, count_display)
|
||||
|
||||
~H"""
|
||||
<div class={[@border_class, @depth > 0 && "mt-4"]} style={"margin-left: #{@margin_left}px"}>
|
||||
@@ -350,12 +381,16 @@ defmodule ComponentsElixirWeb.StorageLocationsLive do
|
||||
<% end %>
|
||||
</button>
|
||||
<% else %>
|
||||
<div class="w-6"></div> <!-- Spacer for alignment -->
|
||||
<div class="w-6"></div>
|
||||
<!-- Spacer for alignment -->
|
||||
<% end %>
|
||||
|
||||
<.icon name={@icon_name} class={"#{@icon_size} #{if @depth == 0, do: "text-primary", else: "text-base-content/60"} mt-0.5"} />
|
||||
|
||||
<!-- Content area - always starts at same vertical position -->
|
||||
<.icon
|
||||
name={@icon_name}
|
||||
class={"#{@icon_size} #{if @depth == 0, do: "text-primary", else: "text-base-content/60"} mt-0.5"}
|
||||
/>
|
||||
|
||||
<!-- Content area - always starts at same vertical position -->
|
||||
<div class="flex-1">
|
||||
<!-- Minimized view (default) -->
|
||||
<%= unless @is_expanded do %>
|
||||
@@ -408,8 +443,8 @@ defmodule ComponentsElixirWeb.StorageLocationsLive do
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<!-- Expanded view -->
|
||||
|
||||
<!-- Expanded view -->
|
||||
<%= if @is_expanded do %>
|
||||
<div class="flex items-start justify-between">
|
||||
<div class="flex-1">
|
||||
@@ -468,8 +503,7 @@ defmodule ComponentsElixirWeb.StorageLocationsLive do
|
||||
class="inline-flex items-center px-3 py-1.5 border border-base-300 rounded-md shadow-sm text-sm font-medium text-base-content bg-base-100 hover:bg-base-200 flex-shrink-0"
|
||||
title="Download AprilTag"
|
||||
>
|
||||
<.icon name="hero-arrow-down-tray" class="w-4 h-4 mr-1.5" />
|
||||
Download
|
||||
<.icon name="hero-arrow-down-tray" class="w-4 h-4 mr-1.5" /> Download
|
||||
</button>
|
||||
<% end %>
|
||||
<div class="flex items-center space-x-2">
|
||||
@@ -495,11 +529,16 @@ defmodule ComponentsElixirWeb.StorageLocationsLive do
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Render children recursively (only when expanded) -->
|
||||
|
||||
<!-- Render children recursively (only when expanded) -->
|
||||
<%= if @is_expanded do %>
|
||||
<%= for child <- @children do %>
|
||||
<.location_item location={child} storage_locations={@storage_locations} expanded_locations={@expanded_locations} depth={@depth + 1} />
|
||||
<.location_item
|
||||
location={child}
|
||||
storage_locations={@storage_locations}
|
||||
expanded_locations={@expanded_locations}
|
||||
depth={@depth + 1}
|
||||
/>
|
||||
<% end %>
|
||||
<% end %>
|
||||
</div>
|
||||
@@ -552,8 +591,8 @@ defmodule ComponentsElixirWeb.StorageLocationsLive do
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Add Location Modal -->
|
||||
|
||||
<!-- Add Location Modal -->
|
||||
<%= if @show_add_form do %>
|
||||
<div class="fixed inset-0 bg-base-content/50 overflow-y-auto h-full w-full z-50">
|
||||
<div class="relative top-20 mx-auto p-5 border border-base-300 w-11/12 md:w-1/2 shadow-lg rounded-md bg-base-100">
|
||||
@@ -589,7 +628,9 @@ defmodule ComponentsElixirWeb.StorageLocationsLive do
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-base-content">AprilTag ID (Optional)</label>
|
||||
<label class="block text-sm font-medium text-base-content">
|
||||
AprilTag ID (Optional)
|
||||
</label>
|
||||
<div class="space-y-2">
|
||||
<div class="flex items-center space-x-2">
|
||||
<input
|
||||
@@ -647,10 +688,12 @@ defmodule ComponentsElixirWeb.StorageLocationsLive do
|
||||
class="w-32"
|
||||
/>
|
||||
<div class="text-xs text-base-content/60">
|
||||
Available IDs: <%= length(@available_apriltag_ids) %> of 587
|
||||
Available IDs: {length(@available_apriltag_ids)} of 587
|
||||
<%= if length(@available_apriltag_ids) < 20 do %>
|
||||
<br/>Next available: <%= @available_apriltag_ids |> Enum.take(10) |> Enum.join(", ") %>
|
||||
<%= if length(@available_apriltag_ids) > 10, do: "..." %>
|
||||
<br />Next available: {@available_apriltag_ids
|
||||
|> Enum.take(10)
|
||||
|> Enum.join(", ")}
|
||||
{if length(@available_apriltag_ids) > 10, do: "..."}
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
@@ -678,8 +721,8 @@ defmodule ComponentsElixirWeb.StorageLocationsLive do
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<!-- Edit Location Modal -->
|
||||
|
||||
<!-- Edit Location Modal -->
|
||||
<%= if @show_edit_form do %>
|
||||
<div class="fixed inset-0 bg-base-content/50 overflow-y-auto h-full w-full z-50">
|
||||
<div class="relative top-20 mx-auto p-5 border border-base-300 w-11/12 md:w-1/2 shadow-lg rounded-md bg-base-100">
|
||||
@@ -773,12 +816,14 @@ defmodule ComponentsElixirWeb.StorageLocationsLive do
|
||||
class="w-32"
|
||||
/>
|
||||
<div class="text-xs text-base-content/60">
|
||||
Available IDs: <%= length(@available_apriltag_ids) %> of 587
|
||||
Available IDs: {length(@available_apriltag_ids)} of 587
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
<p class="text-xs text-base-content/60 mt-1">
|
||||
Current: <%= if @editing_location.apriltag_id, do: "ID #{@editing_location.apriltag_id}", else: "None" %>
|
||||
Current: {if @editing_location.apriltag_id,
|
||||
do: "ID #{@editing_location.apriltag_id}",
|
||||
else: "None"}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -803,8 +848,8 @@ defmodule ComponentsElixirWeb.StorageLocationsLive do
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<!-- AprilTag Scanner Modal -->
|
||||
|
||||
<!-- AprilTag Scanner Modal -->
|
||||
<%= if @apriltag_scanner_open do %>
|
||||
<div class="fixed inset-0 bg-base-content/30 bg-opacity-50 overflow-y-auto h-full w-full z-50">
|
||||
<div class="relative top-10 mx-auto p-5 border border-base-300 w-11/12 md:w-1/2 shadow-lg rounded-md bg-base-100">
|
||||
@@ -818,16 +863,20 @@ defmodule ComponentsElixirWeb.StorageLocationsLive do
|
||||
<.icon name="hero-x-mark" class="w-6 h-6" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- AprilTag Scanner Interface -->
|
||||
|
||||
<!-- AprilTag Scanner Interface -->
|
||||
<div class="border-2 border-dashed border-base-300 rounded-lg p-6 text-center">
|
||||
<.icon name="hero-qr-code" class="mx-auto h-12 w-12 text-base-content/50" />
|
||||
<p class="mt-2 text-sm text-base-content/70">Camera AprilTag scanner would go here</p>
|
||||
<p class="text-xs text-base-content/60 mt-1">In a real implementation, this would use JavaScript AprilTag detection</p>
|
||||
|
||||
<!-- Test buttons for demo -->
|
||||
<p class="text-xs text-base-content/60 mt-1">
|
||||
In a real implementation, this would use JavaScript AprilTag detection
|
||||
</p>
|
||||
|
||||
<!-- Test buttons for demo -->
|
||||
<div class="mt-4 space-y-2">
|
||||
<p class="text-sm font-medium text-base-content/80">Test with sample AprilTag IDs:</p>
|
||||
<p class="text-sm font-medium text-base-content/80">
|
||||
Test with sample AprilTag IDs:
|
||||
</p>
|
||||
<button
|
||||
phx-click="apriltag_scanned"
|
||||
phx-value-apriltag_id="0"
|
||||
@@ -848,8 +897,8 @@ defmodule ComponentsElixirWeb.StorageLocationsLive do
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<!-- Scanned Tags Display -->
|
||||
|
||||
<!-- Scanned Tags Display -->
|
||||
<%= if length(@scanned_tags) > 0 do %>
|
||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-4">
|
||||
<div class="bg-green-50 border border-green-200 rounded-lg p-4">
|
||||
@@ -863,26 +912,35 @@ defmodule ComponentsElixirWeb.StorageLocationsLive do
|
||||
</button>
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<div :for={scan <- @scanned_tags} class="flex items-center justify-between bg-base-100 p-2 rounded border border-base-300">
|
||||
<div
|
||||
:for={scan <- @scanned_tags}
|
||||
class="flex items-center justify-between bg-base-100 p-2 rounded border border-base-300"
|
||||
>
|
||||
<div>
|
||||
<span class="font-medium text-base-content">{location_display_name(scan.location)}</span>
|
||||
<span class="text-sm text-base-content/70 ml-2">(AprilTag ID {scan.apriltag_id})</span>
|
||||
<span class="font-medium text-base-content">
|
||||
{location_display_name(scan.location)}
|
||||
</span>
|
||||
<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 <%= Hierarchical.compute_level(scan.location, &(&1.parent)) %>
|
||||
Level {Hierarchical.compute_level(scan.location, & &1.parent)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<!-- Storage Locations List -->
|
||||
|
||||
<!-- Storage Locations List -->
|
||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-6">
|
||||
<div class="bg-base-100 shadow overflow-hidden sm:rounded-md">
|
||||
<div class="px-6 py-4 border-b border-base-300">
|
||||
<h2 class="text-lg font-medium text-base-content">Storage Location Hierarchy</h2>
|
||||
<p class="text-sm text-base-content/60 mt-1">Manage your physical storage locations and AprilTags</p>
|
||||
<p class="text-sm text-base-content/60 mt-1">
|
||||
Manage your physical storage locations and AprilTags
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<%= if Enum.empty?(@storage_locations) do %>
|
||||
@@ -897,8 +955,7 @@ defmodule ComponentsElixirWeb.StorageLocationsLive do
|
||||
phx-click="show_add_form"
|
||||
class="inline-flex items-center px-4 py-2 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700"
|
||||
>
|
||||
<.icon name="hero-plus" class="w-4 h-4 mr-2" />
|
||||
Add Location
|
||||
<.icon name="hero-plus" class="w-4 h-4 mr-2" /> Add Location
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -907,7 +964,12 @@ defmodule ComponentsElixirWeb.StorageLocationsLive do
|
||||
<!-- Recursive Storage Location Tree -->
|
||||
<%= for location <- root_storage_locations(@storage_locations) do %>
|
||||
<div class="px-6 py-4">
|
||||
<.location_item location={location} storage_locations={@storage_locations} expanded_locations={@expanded_locations} depth={0} />
|
||||
<.location_item
|
||||
location={location}
|
||||
storage_locations={@storage_locations}
|
||||
expanded_locations={@expanded_locations}
|
||||
depth={0}
|
||||
/>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user