refactor(elixir): remove unused is_active field

from storage location
This commit is contained in:
Schuwi
2025-09-17 23:13:45 +02:00
parent 6a1122c3be
commit 5a1775e836
4 changed files with 286 additions and 149 deletions

View File

@@ -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()

View File

@@ -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)

View File

@@ -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
</div>
</div>
</div>
<!-- Filters -->
<!-- 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">
<div class="flex-1">
@@ -553,7 +571,7 @@ defmodule ComponentsElixirWeb.ComponentsLive do
<%= if @sort_frozen do %>
<div class="absolute -bottom-5 left-0 text-xs text-yellow-600 flex items-center transition-opacity duration-200 pointer-events-none">
<svg class="w-3 h-3 mr-1" fill="currentColor" viewBox="0 0 20 20">
<path d="M10 2L3 7v11h14V7l-7-5z"/>
<path d="M10 2L3 7v11h14V7l-7-5z" />
</svg>
Sort temporarily frozen
</div>
@@ -562,7 +580,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">
@@ -578,7 +596,13 @@ defmodule ComponentsElixirWeb.ComponentsLive do
</button>
</div>
<.form for={@form} phx-submit="save_component" phx-change="validate" multipart={true} class="space-y-4">
<.form
for={@form}
phx-submit="save_component"
phx-change="validate"
multipart={true}
class="space-y-4"
>
<div>
<label class="block text-sm font-medium text-base-content">Name</label>
<.input field={@form[:name]} type="text" required />
@@ -605,7 +629,9 @@ defmodule ComponentsElixirWeb.ComponentsLive do
<.input field={@form[:keywords]} type="text" />
</div>
<div>
<label class="block text-sm font-medium text-base-content">Storage Location</label>
<label class="block text-sm font-medium text-base-content">
Storage Location
</label>
<.input
field={@form[:storage_location_id]}
type="select"
@@ -628,14 +654,17 @@ defmodule ComponentsElixirWeb.ComponentsLive do
<div>
<label class="block text-sm font-medium text-base-content">Component Image</label>
<div class="mt-1">
<.live_file_input upload={@uploads.image} class="block w-full text-sm text-base-content file:mr-4 file:py-2 file:px-4 file:rounded-full file:border-0 file:text-sm file:font-semibold file:bg-primary/10 file:text-primary hover:file:bg-primary/20" />
<.live_file_input
upload={@uploads.image}
class="block w-full text-sm text-base-content file:mr-4 file:py-2 file:px-4 file:rounded-full file:border-0 file:text-sm file:font-semibold file:bg-primary/10 file:text-primary hover:file:bg-primary/20"
/>
</div>
<p class="mt-1 text-xs text-base-content/60">
JPG, PNG, GIF up to 5MB
</p>
<%= for err <- upload_errors(@uploads.image) do %>
<p class="text-error text-sm mt-1"><%= Phoenix.Naming.humanize(err) %></p>
<p class="text-error text-sm mt-1">{Phoenix.Naming.humanize(err)}</p>
<% 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" />
</div>
<div class="ml-3">
<p class="text-sm font-medium text-base-content"><%= entry.client_name %></p>
<p class="text-sm text-base-content/60"><%= entry.progress %>%</p>
<p class="text-sm font-medium text-base-content">{entry.client_name}</p>
<p class="text-sm text-base-content/60">{entry.progress}%</p>
</div>
</div>
<button type="button" phx-click="cancel-upload" phx-value-ref={entry.ref} aria-label="cancel" class="text-error hover:text-error/80">
<button
type="button"
phx-click="cancel-upload"
phx-value-ref={entry.ref}
aria-label="cancel"
class="text-error hover:text-error/80"
>
<.icon name="hero-x-mark" class="w-5 h-5" />
</button>
</div>
<% end %>
<%= for err <- upload_errors(@uploads.image) do %>
<p class="mt-1 text-sm text-error"><%= upload_error_to_string(err) %></p>
<p class="mt-1 text-sm text-error">{upload_error_to_string(err)}</p>
<% end %>
</div>
@@ -679,7 +714,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">
@@ -695,7 +730,13 @@ defmodule ComponentsElixirWeb.ComponentsLive do
</button>
</div>
<.form for={@form} phx-submit="save_edit" phx-change="validate" multipart={true} class="space-y-4">
<.form
for={@form}
phx-submit="save_edit"
phx-change="validate"
multipart={true}
class="space-y-4"
>
<div>
<label class="block text-sm font-medium text-base-content">Name</label>
<.input field={@form[:name]} type="text" required />
@@ -722,7 +763,9 @@ defmodule ComponentsElixirWeb.ComponentsLive do
<.input field={@form[:keywords]} type="text" />
</div>
<div>
<label class="block text-sm font-medium text-base-content">Storage Location</label>
<label class="block text-sm font-medium text-base-content">
Storage Location
</label>
<.input
field={@form[:storage_location_id]}
type="select"
@@ -747,18 +790,25 @@ defmodule ComponentsElixirWeb.ComponentsLive do
<%= if @editing_component && @editing_component.image_filename do %>
<div class="mt-1 mb-2">
<p class="text-sm text-base-content/70">Current image:</p>
<img src={"/uploads/images/#{@editing_component.image_filename}"} alt="Current component" class="h-20 w-20 object-cover rounded-lg" />
<img
src={"/uploads/images/#{@editing_component.image_filename}"}
alt="Current component"
class="h-20 w-20 object-cover rounded-lg"
/>
</div>
<% end %>
<div class="mt-1">
<.live_file_input upload={@uploads.image} class="block w-full text-sm text-base-content file:mr-4 file:py-2 file:px-4 file:rounded-full file:border-0 file:text-sm file:font-semibold file:bg-primary/10 file:text-primary hover:file:bg-primary/20" />
<.live_file_input
upload={@uploads.image}
class="block w-full text-sm text-base-content file:mr-4 file:py-2 file:px-4 file:rounded-full file:border-0 file:text-sm file:font-semibold file:bg-primary/10 file:text-primary hover:file:bg-primary/20"
/>
</div>
<p class="mt-1 text-xs text-base-content/60">
JPG, PNG, GIF up to 5MB (leave empty to keep current image)
</p>
<%= for err <- upload_errors(@uploads.image) do %>
<p class="text-error text-sm mt-1"><%= Phoenix.Naming.humanize(err) %></p>
<p class="text-error text-sm mt-1">{Phoenix.Naming.humanize(err)}</p>
<% end %>
<%= for entry <- @uploads.image.entries do %>
@@ -768,17 +818,23 @@ defmodule ComponentsElixirWeb.ComponentsLive do
<.live_img_preview entry={entry} class="h-10 w-10 rounded-lg object-cover" />
</div>
<div class="ml-3">
<p class="text-sm font-medium text-base-content"><%= entry.client_name %></p>
<p class="text-sm text-base-content/60"><%= entry.progress %>%</p>
<p class="text-sm font-medium text-base-content">{entry.client_name}</p>
<p class="text-sm text-base-content/60">{entry.progress}%</p>
</div>
</div>
<button type="button" phx-click="cancel-upload" phx-value-ref={entry.ref} aria-label="cancel" class="text-error hover:text-error/80">
<button
type="button"
phx-click="cancel-upload"
phx-value-ref={entry.ref}
aria-label="cancel"
class="text-error hover:text-error/80"
>
<.icon name="hero-x-mark" class="w-5 h-5" />
</button>
</div>
<% end %>
<%= for err <- upload_errors(@uploads.image) do %>
<p class="mt-1 text-sm text-error"><%= upload_error_to_string(err) %></p>
<p class="mt-1 text-sm text-error">{upload_error_to_string(err)}</p>
<% end %>
</div>
@@ -802,17 +858,28 @@ 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">
<ul class="divide-y divide-base-300" id="components-list" phx-update="replace">
<%= for component <- @components do %>
<li id={"component-#{component.id}"} class={[
"px-6 py-6 hover:bg-base-200 transition-all duration-200",
if(@focused_component_id == component.id, do: "bg-base-50 border-l-4 border-primary", else: "cursor-pointer"),
if(@interacting_with == to_string(component.id), do: "ring-2 ring-yellow-400 ring-opacity-50 bg-yellow-50", else: "")
]} phx-click={if @focused_component_id != component.id, do: "toggle_focus", else: nil} phx-value-id={component.id}>
<li
id={"component-#{component.id}"}
class={[
"px-6 py-6 hover:bg-base-200 transition-all duration-200",
if(@focused_component_id == component.id,
do: "bg-base-50 border-l-4 border-primary",
else: "cursor-pointer"
),
if(@interacting_with == to_string(component.id),
do: "ring-2 ring-yellow-400 ring-opacity-50 bg-yellow-50",
else: ""
)
]}
phx-click={if @focused_component_id != component.id, do: "toggle_focus", else: nil}
phx-value-id={component.id}
>
<%= if @focused_component_id == component.id do %>
<!-- Expanded/Focused View -->
<div class="space-y-6">
@@ -851,8 +918,8 @@ defmodule ComponentsElixirWeb.ComponentsLive do
</button>
</div>
</div>
<!-- Content area with image and details -->
<!-- Content area with image and details -->
<div class="flex gap-6">
<!-- Large Image -->
<div class="flex-shrink-0">
@@ -870,12 +937,15 @@ defmodule ComponentsElixirWeb.ComponentsLive do
</button>
<% else %>
<div class="h-48 w-48 rounded-lg bg-base-200 flex items-center justify-center border border-base-300">
<.icon name="hero-cube-transparent" class="h-20 w-20 text-base-content/50" />
<.icon
name="hero-cube-transparent"
class="h-20 w-20 text-base-content/50"
/>
</div>
<% end %>
</div>
<!-- Details -->
<!-- Details -->
<div class="flex-1 space-y-4 select-text">
<!-- Full Description -->
<%= if component.description do %>
@@ -886,15 +956,20 @@ defmodule ComponentsElixirWeb.ComponentsLive do
</p>
</div>
<% end %>
<!-- Metadata Grid -->
<!-- Metadata Grid -->
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 text-sm">
<%= if component.storage_location do %>
<div class="flex items-start">
<.icon name="hero-map-pin" class="w-4 h-4 mr-2 text-base-content/50 mt-0.5 flex-shrink-0" />
<.icon
name="hero-map-pin"
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">Location:</span>
<div class="text-base-content/70">{storage_location_display_name(component.storage_location)}</div>
<div class="text-base-content/70">
{storage_location_display_name(component.storage_location)}
</div>
</div>
</div>
<% end %>
@@ -906,10 +981,15 @@ defmodule ComponentsElixirWeb.ComponentsLive do
</div>
<div class="flex items-start">
<.icon name="hero-calendar" class="w-4 h-4 mr-2 text-base-content/50 mt-0.5 flex-shrink-0" />
<.icon
name="hero-calendar"
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">Entry Date:</span>
<div class="text-base-content/70">{Calendar.strftime(component.inserted_at, "%B %d, %Y")}</div>
<div class="text-base-content/70">
{Calendar.strftime(component.inserted_at, "%B %d, %Y")}
</div>
</div>
</div>
@@ -933,32 +1013,29 @@ defmodule ComponentsElixirWeb.ComponentsLive do
</div>
</div>
</div>
<!-- Action Buttons -->
<!-- Action Buttons -->
<div class="flex justify-end items-center space-x-2 pt-4 border-t border-base-300">
<button
phx-click="increment_count"
phx-value-id={component.id}
class="inline-flex items-center px-3 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-green-600 hover:bg-green-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-green-500"
>
<.icon name="hero-plus" class="w-4 h-4 mr-1" />
Add
<.icon name="hero-plus" class="w-4 h-4 mr-1" /> Add
</button>
<button
phx-click="decrement_count"
phx-value-id={component.id}
class="inline-flex items-center px-3 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-yellow-600 hover:bg-yellow-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-yellow-500"
>
<.icon name="hero-minus" class="w-4 h-4 mr-1" />
Remove
<.icon name="hero-minus" class="w-4 h-4 mr-1" /> Remove
</button>
<button
phx-click="show_edit_form"
phx-value-id={component.id}
class="inline-flex items-center px-3 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
>
<.icon name="hero-pencil" class="w-4 h-4 mr-1" />
Edit
<.icon name="hero-pencil" class="w-4 h-4 mr-1" /> Edit
</button>
<button
phx-click="delete_component"
@@ -966,8 +1043,7 @@ defmodule ComponentsElixirWeb.ComponentsLive do
data-confirm="Are you sure you want to delete this component?"
class="inline-flex items-center px-3 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-red-600 hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500"
>
<.icon name="hero-trash" class="w-4 h-4 mr-1" />
Delete
<.icon name="hero-trash" class="w-4 h-4 mr-1" /> Delete
</button>
</div>
</div>
@@ -982,7 +1058,11 @@ defmodule ComponentsElixirWeb.ComponentsLive do
phx-value-url={"/user_generated/uploads/images/#{component.image_filename}"}
class="hover:opacity-75 transition-opacity block"
>
<img src={"/user_generated/uploads/images/#{component.image_filename}"} alt={component.name} class="max-h-20 max-w-20 rounded-md object-contain cursor-pointer block" />
<img
src={"/user_generated/uploads/images/#{component.image_filename}"}
alt={component.name}
class="max-h-20 max-w-20 rounded-md object-contain cursor-pointer block"
/>
</button>
<% else %>
<div class="h-20 w-20 rounded-md bg-base-200 flex items-center justify-center">
@@ -1018,8 +1098,8 @@ defmodule ComponentsElixirWeb.ComponentsLive do
</span>
</div>
</div>
<!-- Middle row: Description -->
<!-- Middle row: Description -->
<%= if component.description do %>
<div class="mt-1">
<p class="text-sm text-base-content/70 line-clamp-2">
@@ -1027,14 +1107,19 @@ defmodule ComponentsElixirWeb.ComponentsLive do
</p>
</div>
<% end %>
<!-- Bottom row: Metadata -->
<!-- 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 %>
<div class="flex items-center min-w-0">
<.icon name="hero-map-pin" class="w-4 h-4 mr-1 text-base-content/50 flex-shrink-0" />
<.icon
name="hero-map-pin"
class="w-4 h-4 mr-1 text-base-content/50 flex-shrink-0"
/>
<span class="font-medium">Location:</span>
<span class="ml-1 truncate">{storage_location_display_name(component.storage_location)}</span>
<span class="ml-1 truncate">
{storage_location_display_name(component.storage_location)}
</span>
</div>
<% end %>
<div class="flex items-center">
@@ -1050,8 +1135,8 @@ defmodule ComponentsElixirWeb.ComponentsLive do
</div>
<% end %>
</div>
<!-- Keywords row -->
<!-- Keywords row -->
<%= if component.keywords do %>
<div class="mt-2">
<p class="text-xs text-base-content/50">
@@ -1127,12 +1212,18 @@ defmodule ComponentsElixirWeb.ComponentsLive do
<!-- Image Modal -->
<%= if @show_image_modal do %>
<div class="fixed inset-0 z-50 flex items-center justify-center p-4" phx-click="close_image_modal">
<div
class="fixed inset-0 z-50 flex items-center justify-center p-4"
phx-click="close_image_modal"
>
<!-- 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" phx-click="prevent_close">
<!-- Modal content -->
<div
class="relative bg-base-100 rounded-lg shadow-xl max-w-4xl w-full max-h-full overflow-auto"
phx-click="prevent_close"
>
<!-- Header -->
<div class="flex justify-between items-center p-4 border-b border-base-300 bg-base-100 rounded-t-lg">
<h3 class="text-lg font-semibold text-base-content">Component Image</h3>
@@ -1145,8 +1236,8 @@ defmodule ComponentsElixirWeb.ComponentsLive do
×
</button>
</div>
<!-- Content -->
<!-- Content -->
<div class="p-6 bg-base-100 rounded-b-lg">
<div class="text-center">
<%= if @modal_image_url do %>
@@ -1192,6 +1283,7 @@ defmodule ComponentsElixirWeb.ComponentsLive do
:ok ->
IO.puts("=== DEBUG: File copy successful ===")
{:ok, filename}
{:error, reason} ->
IO.puts("=== DEBUG: File copy failed: #{inspect(reason)} ===")
{:postpone, {:error, reason}}
@@ -1200,18 +1292,21 @@ defmodule ComponentsElixirWeb.ComponentsLive do
IO.inspect(uploaded_files, label: "Uploaded files result")
result = case uploaded_files do
[filename] when is_binary(filename) ->
IO.puts("=== DEBUG: Adding filename to params: #{filename} ===")
Map.put(component_params, "image_filename", filename)
[] ->
IO.puts("=== DEBUG: No files uploaded ===")
component_params
_error ->
IO.puts("=== DEBUG: Upload error ===")
IO.inspect(uploaded_files, label: "Unexpected upload result")
component_params
end
result =
case uploaded_files do
[filename] when is_binary(filename) ->
IO.puts("=== DEBUG: Adding filename to params: #{filename} ===")
Map.put(component_params, "image_filename", filename)
[] ->
IO.puts("=== DEBUG: No files uploaded ===")
component_params
_error ->
IO.puts("=== DEBUG: Upload error ===")
IO.inspect(uploaded_files, label: "Unexpected upload result")
component_params
end
IO.inspect(result, label: "Final component_params")
IO.puts("=== DEBUG: End save_uploaded_image ===")

View File

@@ -0,0 +1,9 @@
defmodule ComponentsElixir.Repo.Migrations.RemoveIsActiveFromStorageLocations do
use Ecto.Migration
def change do
alter table(:storage_locations) do
remove :is_active, :boolean
end
end
end