feat(elixir): add edit component functionality

This commit is contained in:
Schuwi
2025-09-14 12:20:04 +02:00
parent 5e49cb79a0
commit 48c9103058
2 changed files with 169 additions and 29 deletions

View File

@@ -38,6 +38,7 @@ A modern, idiomatic Elixir/Phoenix port of the original PHP-based component inve
- **Category Organization**: Hierarchical category system for better organization
- **Datasheet Links**: Direct links to component datasheets
- **Position Tracking**: Track component storage locations
- **Real-time Updates**: All changes are immediately reflected in the interface
## Setup
@@ -110,6 +111,7 @@ The application uses a simple password-based authentication system:
| `getItems.php` | `Inventory.list_components/1` | Type-safe, composable queries |
| `getCategories.php` | `Inventory.list_categories/0` | Proper associations, hierarchical support |
| `addItem.php` | `Inventory.create_component/1` | Built-in validation, changesets |
| Manual editing | `Inventory.update_component/2` | **NEW**: Full edit functionality with validation |
| `changeAmount.php` | `Inventory.update_component_count/2` | Atomic operations, constraints |
| `imageUpload.php` | (Future: Phoenix file uploads) | Planned improvement |
| Session management | Phoenix sessions + LiveView | Built-in CSRF protection |

View File

@@ -28,6 +28,8 @@ defmodule ComponentsElixirWeb.ComponentsLive do
|> assign(:has_more, false)
|> assign(:loading, false)
|> assign(:show_add_form, false)
|> assign(:show_edit_form, false)
|> assign(:editing_component, nil)
|> assign(:form, nil)
|> load_components()}
end
@@ -136,6 +138,26 @@ defmodule ComponentsElixirWeb.ComponentsLive do
|> assign(:form, nil)}
end
def handle_event("show_edit_form", %{"id" => id}, socket) do
component = Inventory.get_component!(id)
changeset = Inventory.change_component(component)
form = to_form(changeset)
{:noreply,
socket
|> assign(:show_edit_form, true)
|> assign(:editing_component, component)
|> assign(:form, form)}
end
def handle_event("hide_edit_form", _params, socket) do
{:noreply,
socket
|> assign(:show_edit_form, false)
|> assign(:editing_component, nil)
|> assign(:form, nil)}
end
def handle_event("save_component", %{"component" => component_params}, socket) do
case Inventory.create_component(component_params) do
{:ok, _component} ->
@@ -151,6 +173,22 @@ defmodule ComponentsElixirWeb.ComponentsLive do
end
end
def handle_event("save_edit", %{"component" => component_params}, socket) do
case Inventory.update_component(socket.assigns.editing_component, component_params) do
{:ok, _component} ->
{:noreply,
socket
|> put_flash(:info, "Component updated successfully")
|> assign(:show_edit_form, false)
|> assign(:editing_component, nil)
|> assign(:form, nil)
|> load_components()}
{:error, changeset} ->
{:noreply, assign(socket, :form, to_form(changeset))}
end
end
defp load_components(socket, opts \\ []) do
append = Keyword.get(opts, :append, false)
@@ -164,11 +202,12 @@ defmodule ComponentsElixirWeb.ComponentsLive do
%{components: new_components, has_more: has_more} =
Inventory.paginate_components(filters)
components = if append do
socket.assigns.components ++ new_components
else
new_components
end
components =
if append do
socket.assigns.components ++ new_components
else
new_components
end
socket
|> assign(:components, components)
@@ -188,9 +227,9 @@ defmodule ComponentsElixirWeb.ComponentsLive do
defp category_options(categories) do
[{"Select a category", nil}] ++
Enum.map(categories, fn category ->
{category.name, category.id}
end)
Enum.map(categories, fn category ->
{category.name, category.id}
end)
end
@impl true
@@ -206,7 +245,7 @@ defmodule ComponentsElixirWeb.ComponentsLive do
Components Inventory
</h1>
<div class="ml-8 text-sm text-gray-500">
<%= @stats.total_components %> components • <%= @stats.total_stock %> items in stock
{@stats.total_components} components • {@stats.total_stock} items in stock
</div>
</div>
<div class="flex items-center space-x-4">
@@ -214,23 +253,21 @@ defmodule ComponentsElixirWeb.ComponentsLive do
phx-click="show_add_form"
class="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
>
<.icon name="hero-plus" class="w-4 h-4 mr-2" />
Add Component
<.icon name="hero-plus" class="w-4 h-4 mr-2" /> Add Component
</button>
<.link
href="/logout"
method="post"
class="inline-flex items-center px-4 py-2 border border-gray-300 text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
>
<.icon name="hero-arrow-right-on-rectangle" class="w-4 h-4 mr-2" />
Logout
<.icon name="hero-arrow-right-on-rectangle" class="w-4 h-4 mr-2" /> Logout
</.link>
</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="flex flex-col sm:flex-row gap-4">
<div class="flex-1">
@@ -250,19 +287,25 @@ defmodule ComponentsElixirWeb.ComponentsLive do
name="sort_criteria"
class="block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm"
>
<option value="all_not_id" selected={@sort_criteria == "all_not_id"}>All (without IDs)</option>
<option value="all_not_id" selected={@sort_criteria == "all_not_id"}>
All (without IDs)
</option>
<option value="all" selected={@sort_criteria == "all"}>All</option>
<option value="name" selected={@sort_criteria == "name"}>Name</option>
<option value="description" selected={@sort_criteria == "description"}>Description</option>
<option value="description" selected={@sort_criteria == "description"}>
Description
</option>
<option value="id" selected={@sort_criteria == "id"}>ID</option>
<option value="category_id" selected={@sort_criteria == "category_id"}>Category ID</option>
<option value="category_id" selected={@sort_criteria == "category_id"}>
Category ID
</option>
</select>
</form>
</div>
</div>
</div>
<!-- Add Component Modal -->
<!-- Add Component Modal -->
<%= if @show_add_form do %>
<div class="fixed inset-0 bg-gray-600 bg-opacity-50 overflow-y-auto h-full w-full z-50">
<div class="relative top-20 mx-auto p-5 border w-11/12 md:w-1/2 shadow-lg rounded-md bg-white">
@@ -285,7 +328,12 @@ defmodule ComponentsElixirWeb.ComponentsLive do
<div>
<label class="block text-sm font-medium text-gray-700">Category</label>
<.input field={@form[:category_id]} type="select" options={category_options(@categories)} required />
<.input
field={@form[:category_id]}
type="select"
options={category_options(@categories)}
required
/>
</div>
<div>
@@ -336,7 +384,86 @@ defmodule ComponentsElixirWeb.ComponentsLive do
</div>
<% end %>
<!-- Components List -->
<!-- Edit Component Modal -->
<%= if @show_edit_form do %>
<div class="fixed inset-0 bg-gray-600 bg-opacity-50 overflow-y-auto h-full w-full z-50">
<div class="relative top-20 mx-auto p-5 border w-11/12 md:w-1/2 shadow-lg rounded-md bg-white">
<div class="mt-3">
<div class="flex justify-between items-center mb-4">
<h3 class="text-lg font-medium text-gray-900">Edit Component</h3>
<button
phx-click="hide_edit_form"
class="text-gray-400 hover:text-gray-600"
>
<.icon name="hero-x-mark" class="w-6 h-6" />
</button>
</div>
<.form for={@form} phx-submit="save_edit" class="space-y-4">
<div>
<label class="block text-sm font-medium text-gray-700">Name</label>
<.input field={@form[:name]} type="text" required />
</div>
<div>
<label class="block text-sm font-medium text-gray-700">Category</label>
<.input
field={@form[:category_id]}
type="select"
options={category_options(@categories)}
required
/>
</div>
<div>
<label class="block text-sm font-medium text-gray-700">Description</label>
<.input field={@form[:description]} type="textarea" />
</div>
<div class="grid grid-cols-2 gap-4">
<div>
<label class="block text-sm font-medium text-gray-700">Keywords</label>
<.input field={@form[:keywords]} type="text" />
</div>
<div>
<label class="block text-sm font-medium text-gray-700">Position</label>
<.input field={@form[:position]} type="text" />
</div>
</div>
<div class="grid grid-cols-2 gap-4">
<div>
<label class="block text-sm font-medium text-gray-700">Count</label>
<.input field={@form[:count]} type="number" min="0" />
</div>
<div>
<label class="block text-sm font-medium text-gray-700">Datasheet URL</label>
<.input field={@form[:datasheet_url]} type="url" />
</div>
</div>
<div class="flex justify-end space-x-3 pt-4">
<button
type="button"
phx-click="hide_edit_form"
class="px-4 py-2 border border-gray-300 rounded-md text-sm font-medium text-gray-700 hover:bg-gray-50"
>
Cancel
</button>
<button
type="submit"
class="px-4 py-2 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-indigo-600 hover:bg-indigo-700"
>
Update Component
</button>
</div>
</.form>
</div>
</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-white shadow overflow-hidden sm:rounded-md">
<ul class="divide-y divide-gray-200" id="components-list" phx-update="replace">
@@ -347,17 +474,21 @@ defmodule ComponentsElixirWeb.ComponentsLive do
<div class="flex items-center justify-between">
<p class="text-sm font-medium text-indigo-600 truncate">
<%= if component.datasheet_url do %>
<a href={component.datasheet_url} target="_blank" class="hover:text-indigo-500">
<%= component.name %>
<a
href={component.datasheet_url}
target="_blank"
class="hover:text-indigo-500"
>
{component.name}
<.icon name="hero-arrow-top-right-on-square" class="w-4 h-4 inline ml-1" />
</a>
<% else %>
<%= component.name %>
{component.name}
<% end %>
</p>
<div class="ml-2 flex-shrink-0 flex">
<p class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-green-100 text-green-800">
<%= component.category.name %>
{component.category.name}
</p>
</div>
</div>
@@ -365,22 +496,22 @@ defmodule ComponentsElixirWeb.ComponentsLive do
<div class="sm:flex">
<p class="flex items-center text-sm text-gray-500">
<%= if component.description do %>
<%= component.description %>
{component.description}
<% end %>
</p>
</div>
<div class="mt-2 flex items-center text-sm text-gray-500 sm:mt-0">
<%= if component.position do %>
<p class="mr-6">
<span class="font-medium">Position:</span> <%= component.position %>
<span class="font-medium">Position:</span> {component.position}
</p>
<% end %>
<p class="mr-6">
<span class="font-medium">Count:</span> <%= component.count %>
<span class="font-medium">Count:</span> {component.count}
</p>
<%= if @sort_criteria == "all" or @sort_criteria == "id" do %>
<p class="mr-6">
<span class="font-medium">ID:</span> <%= component.id %>
<span class="font-medium">ID:</span> {component.id}
</p>
<% end %>
</div>
@@ -388,7 +519,7 @@ defmodule ComponentsElixirWeb.ComponentsLive do
<%= if component.keywords do %>
<div class="mt-2">
<p class="text-xs text-gray-400">
<span class="font-medium">Keywords:</span> <%= component.keywords %>
<span class="font-medium">Keywords:</span> {component.keywords}
</p>
</div>
<% end %>
@@ -408,6 +539,13 @@ defmodule ComponentsElixirWeb.ComponentsLive do
>
<.icon name="hero-minus" class="w-4 h-4" />
</button>
<button
phx-click="show_edit_form"
phx-value-id={component.id}
class="inline-flex items-center p-1 border border-transparent rounded-full 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" />
</button>
<button
phx-click="delete_component"
phx-value-id={component.id}