refactor(elixir): remove unused is_active field
from storage location
This commit is contained in:
@@ -15,14 +15,16 @@ defmodule ComponentsElixir.Inventory do
|
|||||||
"""
|
"""
|
||||||
def list_storage_locations do
|
def list_storage_locations do
|
||||||
# Get all locations with preloaded parents in a single query
|
# Get all locations with preloaded parents in a single query
|
||||||
locations = StorageLocation
|
locations =
|
||||||
|> order_by([sl], [asc: sl.name])
|
StorageLocation
|
||||||
|> preload(:parent)
|
|> order_by([sl], asc: sl.name)
|
||||||
|> Repo.all()
|
|> preload(:parent)
|
||||||
|
|> Repo.all()
|
||||||
|
|
||||||
# Compute hierarchy fields for all locations efficiently
|
# Compute hierarchy fields for all locations efficiently
|
||||||
processed_locations = compute_hierarchy_fields_batch(locations)
|
processed_locations =
|
||||||
|> Enum.sort_by(&{&1.level, &1.name})
|
compute_hierarchy_fields_batch(locations)
|
||||||
|
|> Enum.sort_by(&{&1.level, &1.name})
|
||||||
|
|
||||||
# Ensure AprilTag SVGs exist for all locations
|
# Ensure AprilTag SVGs exist for all locations
|
||||||
spawn(fn ->
|
spawn(fn ->
|
||||||
@@ -46,24 +48,35 @@ defmodule ComponentsElixir.Inventory do
|
|||||||
end
|
end
|
||||||
|
|
||||||
defp compute_level_efficient(%{parent_id: nil}, _location_map, _depth), do: 0
|
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
|
defp compute_level_efficient(%{parent_id: parent_id}, location_map, depth) when depth < 10 do
|
||||||
case Map.get(location_map, parent_id) 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)
|
parent -> 1 + compute_level_efficient(parent, location_map, depth + 1)
|
||||||
end
|
end
|
||||||
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: 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
|
case Map.get(location_map, parent_id) do
|
||||||
nil -> name # Orphaned record
|
# Orphaned record
|
||||||
|
nil ->
|
||||||
|
name
|
||||||
|
|
||||||
parent ->
|
parent ->
|
||||||
parent_path = compute_path_efficient(parent, location_map, depth + 1)
|
parent_path = compute_path_efficient(parent, location_map, depth + 1)
|
||||||
"#{parent_path}/#{name}"
|
"#{parent_path}/#{name}"
|
||||||
end
|
end
|
||||||
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 """
|
@doc """
|
||||||
Returns the list of root storage locations (no parent).
|
Returns the list of root storage locations (no parent).
|
||||||
@@ -71,7 +84,7 @@ defmodule ComponentsElixir.Inventory do
|
|||||||
def list_root_storage_locations do
|
def list_root_storage_locations do
|
||||||
StorageLocation
|
StorageLocation
|
||||||
|> where([sl], is_nil(sl.parent_id))
|
|> where([sl], is_nil(sl.parent_id))
|
||||||
|> order_by([sl], [asc: sl.name])
|
|> order_by([sl], asc: sl.name)
|
||||||
|> Repo.all()
|
|> Repo.all()
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -79,9 +92,10 @@ defmodule ComponentsElixir.Inventory do
|
|||||||
Gets a single storage location with computed hierarchy fields.
|
Gets a single storage location with computed hierarchy fields.
|
||||||
"""
|
"""
|
||||||
def get_storage_location!(id) do
|
def get_storage_location!(id) do
|
||||||
location = StorageLocation
|
location =
|
||||||
|> preload(:parent)
|
StorageLocation
|
||||||
|> Repo.get!(id)
|
|> preload(:parent)
|
||||||
|
|> Repo.get!(id)
|
||||||
|
|
||||||
# Compute hierarchy fields
|
# Compute hierarchy fields
|
||||||
level = compute_level_for_single(location)
|
level = compute_level_for_single(location)
|
||||||
@@ -91,6 +105,7 @@ defmodule ComponentsElixir.Inventory do
|
|||||||
|
|
||||||
# Simple computation for single location (allows DB queries)
|
# 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: nil}), do: 0
|
||||||
|
|
||||||
defp compute_level_for_single(%{parent_id: parent_id}) do
|
defp compute_level_for_single(%{parent_id: parent_id}) do
|
||||||
case Repo.get(StorageLocation, parent_id) do
|
case Repo.get(StorageLocation, parent_id) do
|
||||||
nil -> 0
|
nil -> 0
|
||||||
@@ -99,6 +114,7 @@ defmodule ComponentsElixir.Inventory do
|
|||||||
end
|
end
|
||||||
|
|
||||||
defp compute_path_for_single(%{parent_id: nil, name: name}), do: name
|
defp compute_path_for_single(%{parent_id: nil, name: name}), do: name
|
||||||
|
|
||||||
defp compute_path_for_single(%{parent_id: parent_id, name: name}) do
|
defp compute_path_for_single(%{parent_id: parent_id, name: name}) do
|
||||||
case Repo.get(StorageLocation, parent_id) do
|
case Repo.get(StorageLocation, parent_id) do
|
||||||
nil -> name
|
nil -> name
|
||||||
@@ -115,7 +131,9 @@ defmodule ComponentsElixir.Inventory do
|
|||||||
|> preload(:parent)
|
|> preload(:parent)
|
||||||
|> Repo.one()
|
|> Repo.one()
|
||||||
|> case do
|
|> case do
|
||||||
nil -> nil
|
nil ->
|
||||||
|
nil
|
||||||
|
|
||||||
location ->
|
location ->
|
||||||
level = compute_level_for_single(location)
|
level = compute_level_for_single(location)
|
||||||
path = compute_path_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
|
# Convert string keys to atoms to maintain consistency
|
||||||
attrs = normalize_string_keys(attrs)
|
attrs = normalize_string_keys(attrs)
|
||||||
|
|
||||||
result = %StorageLocation{}
|
result =
|
||||||
|> StorageLocation.changeset(attrs)
|
%StorageLocation{}
|
||||||
|> Repo.insert()
|
|> StorageLocation.changeset(attrs)
|
||||||
|
|> Repo.insert()
|
||||||
|
|
||||||
case result do
|
case result do
|
||||||
{:ok, location} ->
|
{:ok, location} ->
|
||||||
{:ok, location}
|
{:ok, location}
|
||||||
|
|
||||||
error ->
|
error ->
|
||||||
error
|
error
|
||||||
end
|
end
|
||||||
@@ -149,13 +169,15 @@ defmodule ComponentsElixir.Inventory do
|
|||||||
# Convert string keys to atoms to maintain consistency
|
# Convert string keys to atoms to maintain consistency
|
||||||
attrs = normalize_string_keys(attrs)
|
attrs = normalize_string_keys(attrs)
|
||||||
|
|
||||||
result = storage_location
|
result =
|
||||||
|> StorageLocation.changeset(attrs)
|
storage_location
|
||||||
|> Repo.update()
|
|> StorageLocation.changeset(attrs)
|
||||||
|
|> Repo.update()
|
||||||
|
|
||||||
case result do
|
case result do
|
||||||
{:ok, updated_location} ->
|
{:ok, updated_location} ->
|
||||||
{:ok, updated_location}
|
{:ok, updated_location}
|
||||||
|
|
||||||
error ->
|
error ->
|
||||||
error
|
error
|
||||||
end
|
end
|
||||||
@@ -182,12 +204,14 @@ defmodule ComponentsElixir.Inventory do
|
|||||||
case get_storage_location_by_apriltag_id(apriltag_id) do
|
case get_storage_location_by_apriltag_id(apriltag_id) do
|
||||||
nil ->
|
nil ->
|
||||||
{:error, :not_found}
|
{:error, :not_found}
|
||||||
|
|
||||||
location ->
|
location ->
|
||||||
{:ok, %{
|
{:ok,
|
||||||
type: :storage_location,
|
%{
|
||||||
location: location,
|
type: :storage_location,
|
||||||
apriltag_id: apriltag_id
|
location: location,
|
||||||
}}
|
apriltag_id: apriltag_id
|
||||||
|
}}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -195,6 +219,7 @@ defmodule ComponentsElixir.Inventory do
|
|||||||
Computes the path for a storage location (for display purposes).
|
Computes the path for a storage location (for display purposes).
|
||||||
"""
|
"""
|
||||||
def compute_storage_location_path(nil), do: nil
|
def compute_storage_location_path(nil), do: nil
|
||||||
|
|
||||||
def compute_storage_location_path(%StorageLocation{} = location) do
|
def compute_storage_location_path(%StorageLocation{} = location) do
|
||||||
compute_path_for_single(location)
|
compute_path_for_single(location)
|
||||||
end
|
end
|
||||||
@@ -205,6 +230,7 @@ defmodule ComponentsElixir.Inventory do
|
|||||||
{key, value}, acc when is_binary(key) ->
|
{key, value}, acc when is_binary(key) ->
|
||||||
atom_key = String.to_atom(key)
|
atom_key = String.to_atom(key)
|
||||||
Map.put(acc, atom_key, value)
|
Map.put(acc, atom_key, value)
|
||||||
|
|
||||||
{key, value}, acc ->
|
{key, value}, acc ->
|
||||||
Map.put(acc, key, value)
|
Map.put(acc, key, value)
|
||||||
end)
|
end)
|
||||||
@@ -281,28 +307,33 @@ defmodule ComponentsElixir.Inventory do
|
|||||||
|
|
||||||
{:search, search_term}, query when is_binary(search_term) and search_term != "" ->
|
{:search, search_term}, query when is_binary(search_term) and search_term != "" ->
|
||||||
search_pattern = "%#{search_term}%"
|
search_pattern = "%#{search_term}%"
|
||||||
where(query, [c],
|
|
||||||
|
where(
|
||||||
|
query,
|
||||||
|
[c],
|
||||||
ilike(c.name, ^search_pattern) or
|
ilike(c.name, ^search_pattern) or
|
||||||
ilike(c.description, ^search_pattern) or
|
ilike(c.description, ^search_pattern) or
|
||||||
ilike(c.keywords, ^search_pattern) or
|
ilike(c.keywords, ^search_pattern) or
|
||||||
ilike(c.position, ^search_pattern)
|
ilike(c.position, ^search_pattern)
|
||||||
)
|
)
|
||||||
|
|
||||||
_, query -> query
|
_, query ->
|
||||||
|
query
|
||||||
end)
|
end)
|
||||||
end
|
end
|
||||||
|
|
||||||
defp apply_component_sorting(query, opts) do
|
defp apply_component_sorting(query, opts) do
|
||||||
case Keyword.get(opts, :sort_criteria, "name_asc") do
|
case Keyword.get(opts, :sort_criteria, "name_asc") do
|
||||||
"name_asc" -> order_by(query, [c], [asc: c.name])
|
"name_asc" -> order_by(query, [c], asc: c.name)
|
||||||
"name_desc" -> order_by(query, [c], [desc: c.name])
|
"name_desc" -> order_by(query, [c], desc: c.name)
|
||||||
"inserted_at_asc" -> order_by(query, [c], [asc: c.inserted_at])
|
"inserted_at_asc" -> order_by(query, [c], asc: c.inserted_at)
|
||||||
"inserted_at_desc" -> order_by(query, [c], [desc: 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_asc" -> order_by(query, [c], asc: c.updated_at)
|
||||||
"updated_at_desc" -> order_by(query, [c], [desc: c.updated_at])
|
"updated_at_desc" -> order_by(query, [c], desc: c.updated_at)
|
||||||
"count_asc" -> order_by(query, [c], [asc: c.count])
|
"count_asc" -> order_by(query, [c], asc: c.count)
|
||||||
"count_desc" -> order_by(query, [c], [desc: c.count])
|
"count_desc" -> order_by(query, [c], desc: c.count)
|
||||||
_ -> order_by(query, [c], [asc: c.name]) # Default fallback
|
# Default fallback
|
||||||
|
_ -> order_by(query, [c], asc: c.name)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -353,10 +384,12 @@ defmodule ComponentsElixir.Inventory do
|
|||||||
def get_inventory_stats do
|
def get_inventory_stats do
|
||||||
total_components = Repo.aggregate(Component, :count, :id)
|
total_components = Repo.aggregate(Component, :count, :id)
|
||||||
|
|
||||||
total_stock = Component
|
total_stock =
|
||||||
|
Component
|
||||||
|> Repo.aggregate(:sum, :count)
|
|> Repo.aggregate(:sum, :count)
|
||||||
|
|
||||||
categories_with_components = Component
|
categories_with_components =
|
||||||
|
Component
|
||||||
|> distinct([c], c.category_id)
|
|> distinct([c], c.category_id)
|
||||||
|> Repo.aggregate(:count, :category_id)
|
|> Repo.aggregate(:count, :category_id)
|
||||||
|
|
||||||
@@ -406,6 +439,7 @@ defmodule ComponentsElixir.Inventory do
|
|||||||
"""
|
"""
|
||||||
def decrement_component_count(%Component{} = component) do
|
def decrement_component_count(%Component{} = component) do
|
||||||
new_count = max(0, component.count - 1)
|
new_count = max(0, component.count - 1)
|
||||||
|
|
||||||
component
|
component
|
||||||
|> Component.changeset(%{count: new_count})
|
|> Component.changeset(%{count: new_count})
|
||||||
|> Repo.update()
|
|> Repo.update()
|
||||||
|
|||||||
@@ -15,7 +15,6 @@ defmodule ComponentsElixir.Inventory.StorageLocation do
|
|||||||
field :name, :string
|
field :name, :string
|
||||||
field :description, :string
|
field :description, :string
|
||||||
field :apriltag_id, :integer
|
field :apriltag_id, :integer
|
||||||
field :is_active, :boolean, default: true
|
|
||||||
|
|
||||||
# Computed/virtual fields - not stored in database
|
# Computed/virtual fields - not stored in database
|
||||||
field :level, :integer, virtual: true
|
field :level, :integer, virtual: true
|
||||||
@@ -32,7 +31,7 @@ defmodule ComponentsElixir.Inventory.StorageLocation do
|
|||||||
@doc false
|
@doc false
|
||||||
def changeset(storage_location, attrs) do
|
def changeset(storage_location, attrs) do
|
||||||
storage_location
|
storage_location
|
||||||
|> cast(attrs, [:name, :description, :parent_id, :is_active, :apriltag_id])
|
|> cast(attrs, [:name, :description, :parent_id, :apriltag_id])
|
||||||
|> validate_required([:name])
|
|> validate_required([:name])
|
||||||
|> validate_length(:name, min: 1, max: 100)
|
|> validate_length(:name, min: 1, max: 100)
|
||||||
|> validate_length(:description, max: 500)
|
|> validate_length(:description, max: 500)
|
||||||
|
|||||||
@@ -97,6 +97,7 @@ defmodule ComponentsElixirWeb.ComponentsLive do
|
|||||||
|
|
||||||
def handle_event("category_filter", %{"category_id" => category_id}, socket) do
|
def handle_event("category_filter", %{"category_id" => category_id}, socket) do
|
||||||
category_id = String.to_integer(category_id)
|
category_id = String.to_integer(category_id)
|
||||||
|
|
||||||
{:noreply,
|
{:noreply,
|
||||||
socket
|
socket
|
||||||
|> assign(:selected_category, category_id)
|
|> assign(:selected_category, category_id)
|
||||||
@@ -119,20 +120,26 @@ defmodule ComponentsElixirWeb.ComponentsLive do
|
|||||||
case Inventory.increment_component_count(component) do
|
case Inventory.increment_component_count(component) do
|
||||||
{:ok, _updated_component} ->
|
{:ok, _updated_component} ->
|
||||||
# Only apply sort freeze for dynamic sorting criteria
|
# 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
|
if should_freeze do
|
||||||
# Cancel any existing timer
|
# Cancel any existing timer
|
||||||
if socket.assigns.sort_freeze_timer do
|
if socket.assigns.sort_freeze_timer do
|
||||||
Process.cancel_timer(socket.assigns.sort_freeze_timer)
|
Process.cancel_timer(socket.assigns.sort_freeze_timer)
|
||||||
end
|
end
|
||||||
|
|
||||||
# Set sort freeze for 3 seconds and mark component as interacting
|
# Set sort freeze for 3 seconds and mark component as interacting
|
||||||
freeze_until = DateTime.add(DateTime.utc_now(), 3, :second)
|
freeze_until = DateTime.add(DateTime.utc_now(), 3, :second)
|
||||||
|
|
||||||
# Set new timer to clear interaction state
|
# Set new timer to clear interaction state
|
||||||
timer_ref = Process.send_after(self(), {:clear_interaction, id}, 3000)
|
timer_ref = Process.send_after(self(), {:clear_interaction, id}, 3000)
|
||||||
|
|
||||||
{:noreply,
|
{:noreply,
|
||||||
socket
|
socket
|
||||||
|> put_flash(:info, "Count updated")
|
|> put_flash(:info, "Count updated")
|
||||||
@@ -160,20 +167,26 @@ defmodule ComponentsElixirWeb.ComponentsLive do
|
|||||||
case Inventory.decrement_component_count(component) do
|
case Inventory.decrement_component_count(component) do
|
||||||
{:ok, _updated_component} ->
|
{:ok, _updated_component} ->
|
||||||
# Only apply sort freeze for dynamic sorting criteria
|
# 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
|
if should_freeze do
|
||||||
# Cancel any existing timer
|
# Cancel any existing timer
|
||||||
if socket.assigns.sort_freeze_timer do
|
if socket.assigns.sort_freeze_timer do
|
||||||
Process.cancel_timer(socket.assigns.sort_freeze_timer)
|
Process.cancel_timer(socket.assigns.sort_freeze_timer)
|
||||||
end
|
end
|
||||||
|
|
||||||
# Set sort freeze for 3 seconds and mark component as interacting
|
# Set sort freeze for 3 seconds and mark component as interacting
|
||||||
freeze_until = DateTime.add(DateTime.utc_now(), 3, :second)
|
freeze_until = DateTime.add(DateTime.utc_now(), 3, :second)
|
||||||
|
|
||||||
# Set new timer to clear interaction state
|
# Set new timer to clear interaction state
|
||||||
timer_ref = Process.send_after(self(), {:clear_interaction, id}, 3000)
|
timer_ref = Process.send_after(self(), {:clear_interaction, id}, 3000)
|
||||||
|
|
||||||
{:noreply,
|
{:noreply,
|
||||||
socket
|
socket
|
||||||
|> put_flash(:info, "Count updated")
|
|> put_flash(:info, "Count updated")
|
||||||
@@ -269,9 +282,11 @@ defmodule ComponentsElixirWeb.ComponentsLive do
|
|||||||
|
|
||||||
new_focused_id =
|
new_focused_id =
|
||||||
if socket.assigns.focused_component_id == component_id do
|
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
|
else
|
||||||
component_id # Focus on the new component
|
# Focus on the new component
|
||||||
|
component_id
|
||||||
end
|
end
|
||||||
|
|
||||||
{:noreply, assign(socket, :focused_component_id, new_focused_id)}
|
{:noreply, assign(socket, :focused_component_id, new_focused_id)}
|
||||||
@@ -349,23 +364,26 @@ defmodule ComponentsElixirWeb.ComponentsLive do
|
|||||||
|
|
||||||
# Check if sorting should be frozen
|
# Check if sorting should be frozen
|
||||||
now = DateTime.utc_now()
|
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
|
if should_reload do
|
||||||
# Normal loading - query database with current sort criteria
|
# Normal loading - query database with current sort criteria
|
||||||
filters = [
|
filters =
|
||||||
search: socket.assigns.search,
|
[
|
||||||
sort_criteria: socket.assigns.sort_criteria,
|
search: socket.assigns.search,
|
||||||
category_id: socket.assigns.selected_category,
|
sort_criteria: socket.assigns.sort_criteria,
|
||||||
limit: @items_per_page,
|
category_id: socket.assigns.selected_category,
|
||||||
offset: socket.assigns.offset
|
limit: @items_per_page,
|
||||||
]
|
offset: socket.assigns.offset
|
||||||
|> Enum.reject(fn
|
]
|
||||||
{_, v} when is_nil(v) -> true
|
|> Enum.reject(fn
|
||||||
{:search, v} when v == "" -> true
|
{_, v} when is_nil(v) -> true
|
||||||
{_, _} -> false
|
{:search, v} when v == "" -> true
|
||||||
end)
|
{_, _} -> false
|
||||||
|
end)
|
||||||
|
|
||||||
%{components: new_components, has_more: has_more} =
|
%{components: new_components, has_more: has_more} =
|
||||||
Inventory.paginate_components(filters)
|
Inventory.paginate_components(filters)
|
||||||
@@ -383,7 +401,7 @@ defmodule ComponentsElixirWeb.ComponentsLive do
|
|||||||
else
|
else
|
||||||
# Frozen - just update the specific component in place without reordering
|
# Frozen - just update the specific component in place without reordering
|
||||||
if socket.assigns.interacting_with do
|
if socket.assigns.interacting_with do
|
||||||
updated_components =
|
updated_components =
|
||||||
Enum.map(socket.assigns.components, fn component ->
|
Enum.map(socket.assigns.components, fn component ->
|
||||||
if to_string(component.id) == socket.assigns.interacting_with do
|
if to_string(component.id) == socket.assigns.interacting_with do
|
||||||
# Reload this specific component to get updated count
|
# Reload this specific component to get updated count
|
||||||
@@ -392,7 +410,7 @@ defmodule ComponentsElixirWeb.ComponentsLive do
|
|||||||
component
|
component
|
||||||
end
|
end
|
||||||
end)
|
end)
|
||||||
|
|
||||||
assign(socket, :components, updated_components)
|
assign(socket, :components, updated_components)
|
||||||
else
|
else
|
||||||
socket
|
socket
|
||||||
@@ -485,8 +503,8 @@ defmodule ComponentsElixirWeb.ComponentsLive do
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Filters -->
|
<!-- Filters -->
|
||||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-6">
|
<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 flex-col sm:flex-row gap-4">
|
||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
@@ -553,7 +571,7 @@ defmodule ComponentsElixirWeb.ComponentsLive do
|
|||||||
<%= if @sort_frozen 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">
|
<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">
|
<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>
|
</svg>
|
||||||
Sort temporarily frozen
|
Sort temporarily frozen
|
||||||
</div>
|
</div>
|
||||||
@@ -562,7 +580,7 @@ defmodule ComponentsElixirWeb.ComponentsLive do
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Add Component Modal -->
|
<!-- Add Component Modal -->
|
||||||
<%= if @show_add_form do %>
|
<%= 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="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>
|
</button>
|
||||||
</div>
|
</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>
|
<div>
|
||||||
<label class="block text-sm font-medium text-base-content">Name</label>
|
<label class="block text-sm font-medium text-base-content">Name</label>
|
||||||
<.input field={@form[:name]} type="text" required />
|
<.input field={@form[:name]} type="text" required />
|
||||||
@@ -605,7 +629,9 @@ defmodule ComponentsElixirWeb.ComponentsLive do
|
|||||||
<.input field={@form[:keywords]} type="text" />
|
<.input field={@form[:keywords]} type="text" />
|
||||||
</div>
|
</div>
|
||||||
<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
|
<.input
|
||||||
field={@form[:storage_location_id]}
|
field={@form[:storage_location_id]}
|
||||||
type="select"
|
type="select"
|
||||||
@@ -628,14 +654,17 @@ defmodule ComponentsElixirWeb.ComponentsLive do
|
|||||||
<div>
|
<div>
|
||||||
<label class="block text-sm font-medium text-base-content">Component Image</label>
|
<label class="block text-sm font-medium text-base-content">Component Image</label>
|
||||||
<div class="mt-1">
|
<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>
|
</div>
|
||||||
<p class="mt-1 text-xs text-base-content/60">
|
<p class="mt-1 text-xs text-base-content/60">
|
||||||
JPG, PNG, GIF up to 5MB
|
JPG, PNG, GIF up to 5MB
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<%= for err <- upload_errors(@uploads.image) do %>
|
<%= 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 %>
|
<% end %>
|
||||||
|
|
||||||
<%= for entry <- @uploads.image.entries do %>
|
<%= 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" />
|
<.live_img_preview entry={entry} class="h-10 w-10 rounded-lg object-cover" />
|
||||||
</div>
|
</div>
|
||||||
<div class="ml-3">
|
<div class="ml-3">
|
||||||
<p class="text-sm font-medium text-base-content"><%= entry.client_name %></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>
|
<p class="text-sm text-base-content/60">{entry.progress}%</p>
|
||||||
</div>
|
</div>
|
||||||
</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" />
|
<.icon name="hero-x-mark" class="w-5 h-5" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<% end %>
|
<% end %>
|
||||||
<%= for err <- upload_errors(@uploads.image) do %>
|
<%= 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 %>
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -679,7 +714,7 @@ defmodule ComponentsElixirWeb.ComponentsLive do
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|
||||||
<!-- Edit Component Modal -->
|
<!-- Edit Component Modal -->
|
||||||
<%= if @show_edit_form do %>
|
<%= 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="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>
|
</button>
|
||||||
</div>
|
</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>
|
<div>
|
||||||
<label class="block text-sm font-medium text-base-content">Name</label>
|
<label class="block text-sm font-medium text-base-content">Name</label>
|
||||||
<.input field={@form[:name]} type="text" required />
|
<.input field={@form[:name]} type="text" required />
|
||||||
@@ -722,7 +763,9 @@ defmodule ComponentsElixirWeb.ComponentsLive do
|
|||||||
<.input field={@form[:keywords]} type="text" />
|
<.input field={@form[:keywords]} type="text" />
|
||||||
</div>
|
</div>
|
||||||
<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
|
<.input
|
||||||
field={@form[:storage_location_id]}
|
field={@form[:storage_location_id]}
|
||||||
type="select"
|
type="select"
|
||||||
@@ -747,18 +790,25 @@ defmodule ComponentsElixirWeb.ComponentsLive do
|
|||||||
<%= if @editing_component && @editing_component.image_filename do %>
|
<%= if @editing_component && @editing_component.image_filename do %>
|
||||||
<div class="mt-1 mb-2">
|
<div class="mt-1 mb-2">
|
||||||
<p class="text-sm text-base-content/70">Current image:</p>
|
<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>
|
</div>
|
||||||
<% end %>
|
<% end %>
|
||||||
<div class="mt-1">
|
<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>
|
</div>
|
||||||
<p class="mt-1 text-xs text-base-content/60">
|
<p class="mt-1 text-xs text-base-content/60">
|
||||||
JPG, PNG, GIF up to 5MB (leave empty to keep current image)
|
JPG, PNG, GIF up to 5MB (leave empty to keep current image)
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<%= for err <- upload_errors(@uploads.image) do %>
|
<%= 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 %>
|
<% end %>
|
||||||
|
|
||||||
<%= for entry <- @uploads.image.entries do %>
|
<%= 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" />
|
<.live_img_preview entry={entry} class="h-10 w-10 rounded-lg object-cover" />
|
||||||
</div>
|
</div>
|
||||||
<div class="ml-3">
|
<div class="ml-3">
|
||||||
<p class="text-sm font-medium text-base-content"><%= entry.client_name %></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>
|
<p class="text-sm text-base-content/60">{entry.progress}%</p>
|
||||||
</div>
|
</div>
|
||||||
</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" />
|
<.icon name="hero-x-mark" class="w-5 h-5" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<% end %>
|
<% end %>
|
||||||
<%= for err <- upload_errors(@uploads.image) do %>
|
<%= 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 %>
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -802,17 +858,28 @@ defmodule ComponentsElixirWeb.ComponentsLive do
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|
||||||
<!-- Components List -->
|
<!-- Components List -->
|
||||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 pb-6">
|
<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">
|
<div class="bg-base-100 shadow overflow-hidden sm:rounded-md">
|
||||||
<ul class="divide-y divide-base-300" id="components-list" phx-update="replace">
|
<ul class="divide-y divide-base-300" id="components-list" phx-update="replace">
|
||||||
<%= for component <- @components do %>
|
<%= for component <- @components do %>
|
||||||
<li id={"component-#{component.id}"} class={[
|
<li
|
||||||
"px-6 py-6 hover:bg-base-200 transition-all duration-200",
|
id={"component-#{component.id}"}
|
||||||
if(@focused_component_id == component.id, do: "bg-base-50 border-l-4 border-primary", else: "cursor-pointer"),
|
class={[
|
||||||
if(@interacting_with == to_string(component.id), do: "ring-2 ring-yellow-400 ring-opacity-50 bg-yellow-50", else: "")
|
"px-6 py-6 hover:bg-base-200 transition-all duration-200",
|
||||||
]} 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: "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 %>
|
<%= if @focused_component_id == component.id do %>
|
||||||
<!-- Expanded/Focused View -->
|
<!-- Expanded/Focused View -->
|
||||||
<div class="space-y-6">
|
<div class="space-y-6">
|
||||||
@@ -851,8 +918,8 @@ defmodule ComponentsElixirWeb.ComponentsLive do
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Content area with image and details -->
|
<!-- Content area with image and details -->
|
||||||
<div class="flex gap-6">
|
<div class="flex gap-6">
|
||||||
<!-- Large Image -->
|
<!-- Large Image -->
|
||||||
<div class="flex-shrink-0">
|
<div class="flex-shrink-0">
|
||||||
@@ -870,12 +937,15 @@ defmodule ComponentsElixirWeb.ComponentsLive do
|
|||||||
</button>
|
</button>
|
||||||
<% else %>
|
<% else %>
|
||||||
<div class="h-48 w-48 rounded-lg bg-base-200 flex items-center justify-center border border-base-300">
|
<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>
|
</div>
|
||||||
<% end %>
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Details -->
|
<!-- Details -->
|
||||||
<div class="flex-1 space-y-4 select-text">
|
<div class="flex-1 space-y-4 select-text">
|
||||||
<!-- Full Description -->
|
<!-- Full Description -->
|
||||||
<%= if component.description do %>
|
<%= if component.description do %>
|
||||||
@@ -886,15 +956,20 @@ defmodule ComponentsElixirWeb.ComponentsLive do
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|
||||||
<!-- Metadata Grid -->
|
<!-- Metadata Grid -->
|
||||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 text-sm">
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 text-sm">
|
||||||
<%= if component.storage_location do %>
|
<%= if component.storage_location do %>
|
||||||
<div class="flex items-start">
|
<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>
|
<div>
|
||||||
<span class="font-medium text-base-content">Location:</span>
|
<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>
|
||||||
</div>
|
</div>
|
||||||
<% end %>
|
<% end %>
|
||||||
@@ -906,10 +981,15 @@ defmodule ComponentsElixirWeb.ComponentsLive do
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex items-start">
|
<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>
|
<div>
|
||||||
<span class="font-medium text-base-content">Entry Date:</span>
|
<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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -933,32 +1013,29 @@ defmodule ComponentsElixirWeb.ComponentsLive do
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Action Buttons -->
|
<!-- Action Buttons -->
|
||||||
<div class="flex justify-end items-center space-x-2 pt-4 border-t border-base-300">
|
<div class="flex justify-end items-center space-x-2 pt-4 border-t border-base-300">
|
||||||
<button
|
<button
|
||||||
phx-click="increment_count"
|
phx-click="increment_count"
|
||||||
phx-value-id={component.id}
|
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"
|
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" />
|
<.icon name="hero-plus" class="w-4 h-4 mr-1" /> Add
|
||||||
Add
|
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
phx-click="decrement_count"
|
phx-click="decrement_count"
|
||||||
phx-value-id={component.id}
|
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"
|
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" />
|
<.icon name="hero-minus" class="w-4 h-4 mr-1" /> Remove
|
||||||
Remove
|
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
phx-click="show_edit_form"
|
phx-click="show_edit_form"
|
||||||
phx-value-id={component.id}
|
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"
|
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" />
|
<.icon name="hero-pencil" class="w-4 h-4 mr-1" /> Edit
|
||||||
Edit
|
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
phx-click="delete_component"
|
phx-click="delete_component"
|
||||||
@@ -966,8 +1043,7 @@ defmodule ComponentsElixirWeb.ComponentsLive do
|
|||||||
data-confirm="Are you sure you want to delete this component?"
|
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"
|
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" />
|
<.icon name="hero-trash" class="w-4 h-4 mr-1" /> Delete
|
||||||
Delete
|
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -982,7 +1058,11 @@ defmodule ComponentsElixirWeb.ComponentsLive do
|
|||||||
phx-value-url={"/user_generated/uploads/images/#{component.image_filename}"}
|
phx-value-url={"/user_generated/uploads/images/#{component.image_filename}"}
|
||||||
class="hover:opacity-75 transition-opacity block"
|
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>
|
</button>
|
||||||
<% else %>
|
<% else %>
|
||||||
<div class="h-20 w-20 rounded-md bg-base-200 flex items-center justify-center">
|
<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>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Middle row: Description -->
|
<!-- Middle row: Description -->
|
||||||
<%= if component.description do %>
|
<%= if component.description do %>
|
||||||
<div class="mt-1">
|
<div class="mt-1">
|
||||||
<p class="text-sm text-base-content/70 line-clamp-2">
|
<p class="text-sm text-base-content/70 line-clamp-2">
|
||||||
@@ -1027,14 +1107,19 @@ defmodule ComponentsElixirWeb.ComponentsLive do
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<% end %>
|
<% 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">
|
<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 %>
|
<%= if component.storage_location do %>
|
||||||
<div class="flex items-center min-w-0">
|
<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="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>
|
</div>
|
||||||
<% end %>
|
<% end %>
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
@@ -1050,8 +1135,8 @@ defmodule ComponentsElixirWeb.ComponentsLive do
|
|||||||
</div>
|
</div>
|
||||||
<% end %>
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Keywords row -->
|
<!-- Keywords row -->
|
||||||
<%= if component.keywords do %>
|
<%= if component.keywords do %>
|
||||||
<div class="mt-2">
|
<div class="mt-2">
|
||||||
<p class="text-xs text-base-content/50">
|
<p class="text-xs text-base-content/50">
|
||||||
@@ -1127,12 +1212,18 @@ defmodule ComponentsElixirWeb.ComponentsLive do
|
|||||||
|
|
||||||
<!-- Image Modal -->
|
<!-- Image Modal -->
|
||||||
<%= if @show_image_modal do %>
|
<%= 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 -->
|
<!-- Background overlay -->
|
||||||
<div class="absolute inset-0 bg-black bg-opacity-75"></div>
|
<div class="absolute inset-0 bg-black bg-opacity-75"></div>
|
||||||
|
|
||||||
<!-- Modal content -->
|
<!-- 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">
|
<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 -->
|
<!-- Header -->
|
||||||
<div class="flex justify-between items-center p-4 border-b border-base-300 bg-base-100 rounded-t-lg">
|
<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>
|
<h3 class="text-lg font-semibold text-base-content">Component Image</h3>
|
||||||
@@ -1145,8 +1236,8 @@ defmodule ComponentsElixirWeb.ComponentsLive do
|
|||||||
×
|
×
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Content -->
|
<!-- Content -->
|
||||||
<div class="p-6 bg-base-100 rounded-b-lg">
|
<div class="p-6 bg-base-100 rounded-b-lg">
|
||||||
<div class="text-center">
|
<div class="text-center">
|
||||||
<%= if @modal_image_url do %>
|
<%= if @modal_image_url do %>
|
||||||
@@ -1192,6 +1283,7 @@ defmodule ComponentsElixirWeb.ComponentsLive do
|
|||||||
:ok ->
|
:ok ->
|
||||||
IO.puts("=== DEBUG: File copy successful ===")
|
IO.puts("=== DEBUG: File copy successful ===")
|
||||||
{:ok, filename}
|
{:ok, filename}
|
||||||
|
|
||||||
{:error, reason} ->
|
{:error, reason} ->
|
||||||
IO.puts("=== DEBUG: File copy failed: #{inspect(reason)} ===")
|
IO.puts("=== DEBUG: File copy failed: #{inspect(reason)} ===")
|
||||||
{:postpone, {:error, reason}}
|
{:postpone, {:error, reason}}
|
||||||
@@ -1200,18 +1292,21 @@ defmodule ComponentsElixirWeb.ComponentsLive do
|
|||||||
|
|
||||||
IO.inspect(uploaded_files, label: "Uploaded files result")
|
IO.inspect(uploaded_files, label: "Uploaded files result")
|
||||||
|
|
||||||
result = case uploaded_files do
|
result =
|
||||||
[filename] when is_binary(filename) ->
|
case uploaded_files do
|
||||||
IO.puts("=== DEBUG: Adding filename to params: #{filename} ===")
|
[filename] when is_binary(filename) ->
|
||||||
Map.put(component_params, "image_filename", 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: No files uploaded ===")
|
||||||
IO.puts("=== DEBUG: Upload error ===")
|
component_params
|
||||||
IO.inspect(uploaded_files, label: "Unexpected upload result")
|
|
||||||
component_params
|
_error ->
|
||||||
end
|
IO.puts("=== DEBUG: Upload error ===")
|
||||||
|
IO.inspect(uploaded_files, label: "Unexpected upload result")
|
||||||
|
component_params
|
||||||
|
end
|
||||||
|
|
||||||
IO.inspect(result, label: "Final component_params")
|
IO.inspect(result, label: "Final component_params")
|
||||||
IO.puts("=== DEBUG: End save_uploaded_image ===")
|
IO.puts("=== DEBUG: End save_uploaded_image ===")
|
||||||
|
|||||||
@@ -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
|
||||||
Reference in New Issue
Block a user