diff --git a/README.md b/README.md index 4bc444c..9f9f1ed 100644 --- a/README.md +++ b/README.md @@ -36,6 +36,7 @@ A modern, idiomatic Elixir/Phoenix port of the original PHP-based component inve - **Inventory Tracking**: Monitor component quantities with increment/decrement buttons - **Search & Filter**: Fast search across component names, descriptions, and keywords - **Category Organization**: Hierarchical category system for better organization +- **Category Management**: Add, edit, delete categories through the web interface with hierarchical support - **Datasheet Links**: Direct links to component datasheets - **Position Tracking**: Track component storage locations - **Real-time Updates**: All changes are immediately reflected in the interface @@ -97,6 +98,7 @@ The application uses a simple password-based authentication system: ### Live Views - **`ComponentsElixirWeb.LoginLive`**: Authentication interface - **`ComponentsElixirWeb.ComponentsLive`**: Main component management interface +- **`ComponentsElixirWeb.CategoriesLive`**: Category management interface ### Key Features - **Real-time updates**: Changes are immediately reflected without page refresh @@ -113,18 +115,18 @@ The application uses a simple password-based authentication system: | `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 | +| Manual category management | `CategoriesLive` + `Inventory.create_category/1` | **NEW**: Full category CRUD with web interface | | `imageUpload.php` | (Future: Phoenix file uploads) | Planned improvement | | Session management | Phoenix sessions + LiveView | Built-in CSRF protection | ## Future Enhancements 1. **Image Upload**: Implement Phoenix file uploads for component images -2. **Category Management UI**: Add/edit/delete categories through the web interface -3. **Bulk Operations**: Import/export components via CSV -4. **API Endpoints**: REST API for external integrations -5. **User Management**: Multi-user support with roles and permissions -6. **Advanced Search**: Filters by category, stock level, etc. -7. **Barcode/QR Codes**: Generate and scan codes for quick inventory updates +2. **Bulk Operations**: Import/export components via CSV +3. **API Endpoints**: REST API for external integrations +4. **User Management**: Multi-user support with roles and permissions +5. **Advanced Search**: Filters by category, stock level, etc. +6. **Barcode/QR Codes**: Generate and scan codes for quick inventory updates ## Development diff --git a/lib/components_elixir/inventory.ex b/lib/components_elixir/inventory.ex index 2163aef..30d7a2a 100644 --- a/lib/components_elixir/inventory.ex +++ b/lib/components_elixir/inventory.ex @@ -81,6 +81,15 @@ defmodule ComponentsElixir.Inventory do Category.changeset(category, attrs) end + @doc """ + Returns the count of components in a specific category. + """ + def count_components_in_category(category_id) do + Component + |> where([c], c.category_id == ^category_id) + |> Repo.aggregate(:count, :id) + end + ## Components @doc """ diff --git a/lib/components_elixir_web/live/categories_live.ex b/lib/components_elixir_web/live/categories_live.ex new file mode 100644 index 0000000..fa402d5 --- /dev/null +++ b/lib/components_elixir_web/live/categories_live.ex @@ -0,0 +1,420 @@ +defmodule ComponentsElixirWeb.CategoriesLive do + use ComponentsElixirWeb, :live_view + + alias ComponentsElixir.{Inventory, Auth} + alias ComponentsElixir.Inventory.Category + + @impl true + def mount(_params, session, socket) do + # Check authentication + unless Auth.authenticated?(session) do + {:ok, socket |> push_navigate(to: ~p"/login")} + else + categories = Inventory.list_categories() + + {:ok, + socket + |> assign(:session, session) + |> assign(:categories, categories) + |> assign(:show_add_form, false) + |> assign(:show_edit_form, false) + |> assign(:editing_category, nil) + |> assign(:form, nil) + |> assign(:page_title, "Category Management")} + end + end + + @impl true + def handle_event("show_add_form", _params, socket) do + changeset = Inventory.change_category(%Category{}) + form = to_form(changeset) + + {:noreply, + socket + |> assign(:show_add_form, true) + |> assign(:form, form)} + end + + def handle_event("hide_add_form", _params, socket) do + {:noreply, + socket + |> assign(:show_add_form, false) + |> assign(:form, nil)} + end + + def handle_event("show_edit_form", %{"id" => id}, socket) do + category = Inventory.get_category!(id) + changeset = Inventory.change_category(category) + form = to_form(changeset) + + {:noreply, + socket + |> assign(:show_edit_form, true) + |> assign(:editing_category, category) + |> assign(:form, form)} + end + + def handle_event("hide_edit_form", _params, socket) do + {:noreply, + socket + |> assign(:show_edit_form, false) + |> assign(:editing_category, nil) + |> assign(:form, nil)} + end + + def handle_event("save_category", %{"category" => category_params}, socket) do + case Inventory.create_category(category_params) do + {:ok, _category} -> + {:noreply, + socket + |> put_flash(:info, "Category created successfully") + |> assign(:show_add_form, false) + |> assign(:form, nil) + |> reload_categories()} + + {:error, changeset} -> + {:noreply, assign(socket, :form, to_form(changeset))} + end + end + + def handle_event("save_edit", %{"category" => category_params}, socket) do + case Inventory.update_category(socket.assigns.editing_category, category_params) do + {:ok, _category} -> + {:noreply, + socket + |> put_flash(:info, "Category updated successfully") + |> assign(:show_edit_form, false) + |> assign(:editing_category, nil) + |> assign(:form, nil) + |> reload_categories()} + + {:error, changeset} -> + {:noreply, assign(socket, :form, to_form(changeset))} + end + end + + def handle_event("delete_category", %{"id" => id}, socket) do + category = Inventory.get_category!(id) + + case Inventory.delete_category(category) do + {:ok, _deleted_category} -> + {:noreply, + socket + |> put_flash(:info, "Category deleted successfully") + |> reload_categories()} + + {:error, _changeset} -> + {:noreply, put_flash(socket, :error, "Cannot delete category - it may have components assigned or child categories")} + end + end + + defp reload_categories(socket) do + categories = Inventory.list_categories() + assign(socket, :categories, categories) + end + + defp parent_category_options(categories, editing_category_id \\ nil) do + available_categories = + categories + |> Enum.reject(fn cat -> + cat.id == editing_category_id || + (editing_category_id && is_descendant?(categories, cat.id, editing_category_id)) + end) + |> Enum.map(fn category -> + {category_display_name(category), category.id} + end) + + [{"No parent (Root category)", nil}] ++ available_categories + end + + defp is_descendant?(categories, potential_ancestor_id, category_id) do + # Prevent circular references by checking if the potential parent is a descendant + category = Enum.find(categories, fn cat -> cat.id == category_id end) + + case category do + nil -> false + %{parent_id: nil} -> false + %{parent_id: parent_id} when parent_id == potential_ancestor_id -> true + %{parent_id: parent_id} -> is_descendant?(categories, potential_ancestor_id, parent_id) + end + end + + defp category_display_name(category) do + if category.parent do + "#{category.parent.name} > #{category.name}" + else + category.name + end + end + + defp root_categories(categories) do + Enum.filter(categories, fn cat -> is_nil(cat.parent_id) end) + end + + defp child_categories(categories, parent_id) do + Enum.filter(categories, fn cat -> cat.parent_id == parent_id end) + end + + defp count_components_in_category(category_id) do + Inventory.count_components_in_category(category_id) + end + + @impl true + def render(assigns) do + ~H""" +
Manage your component categories and subcategories
++ Get started by creating your first category. +
+{category.description}
+ <% end %> ++ {count_components_in_category(category.id)} components +
+{child.description}
+ <% end %> ++ {count_components_in_category(child.id)} components +
+