feat: filter by category/location on click

- add filtering by storage location
This commit is contained in:
Schuwi
2025-09-19 22:12:58 +02:00
parent 288d84614a
commit c4a0b41e7d
4 changed files with 221 additions and 12 deletions

View File

@@ -227,9 +227,23 @@ defmodule ComponentsElixirWeb.CategoriesLive do
<div class="flex items-center justify-between">
<div class="flex items-center space-x-3">
<%= if @title_tag == "h3" do %>
<h3 class={"#{@text_size} font-medium #{if @depth == 0, do: "text-base-content", else: "text-base-content/80"}"}>{@category.name}</h3>
<h3 class={"#{@text_size} font-medium"}>
<.link
navigate={~p"/?category_id=#{@category.id}"}
class={"#{if @depth == 0, do: "text-base-content", else: "text-base-content/80"} hover:text-primary hover:underline"}
>
{@category.name}
</.link>
</h3>
<% else %>
<h4 class={"#{@text_size} font-medium #{if @depth == 0, do: "text-base-content", else: "text-base-content/80"}"}>{@category.name}</h4>
<h4 class={"#{@text_size} font-medium"}>
<.link
navigate={~p"/?category_id=#{@category.id}"}
class={"#{if @depth == 0, do: "text-base-content", else: "text-base-content/80"} hover:text-primary hover:underline"}
>
{@category.name}
</.link>
</h4>
<% end %>
<span class="text-xs text-base-content/50">
({@count_display})
@@ -260,9 +274,23 @@ defmodule ComponentsElixirWeb.CategoriesLive do
<div class="flex items-start justify-between">
<div class="flex-1">
<%= if @title_tag == "h3" do %>
<h3 class={"#{@text_size} font-medium #{if @depth == 0, do: "text-base-content", else: "text-base-content/80"}"}>{@category.name}</h3>
<h3 class={"#{@text_size} font-medium"}>
<.link
navigate={~p"/?category_id=#{@category.id}"}
class={"#{if @depth == 0, do: "text-base-content", else: "text-base-content/80"} hover:text-primary hover:underline"}
>
{@category.name}
</.link>
</h3>
<% else %>
<h4 class={"#{@text_size} font-medium #{if @depth == 0, do: "text-base-content", else: "text-base-content/80"}"}>{@category.name}</h4>
<h4 class={"#{@text_size} font-medium"}>
<.link
navigate={~p"/?category_id=#{@category.id}"}
class={"#{if @depth == 0, do: "text-base-content", else: "text-base-content/80"} hover:text-primary hover:underline"}
>
{@category.name}
</.link>
</h4>
<% end %>
<%= if @category.description do %>
<p class="text-sm text-base-content/60 mt-1">{@category.description}</p>

View File

@@ -25,6 +25,8 @@ defmodule ComponentsElixirWeb.ComponentsLive do
|> assign(:search, "")
|> assign(:sort_criteria, "name_asc")
|> assign(:selected_category, nil)
|> assign(:selected_storage_location, nil)
|> assign(:show_advanced_filters, false)
|> assign(:offset, 0)
|> assign(:components, [])
|> assign(:has_more, false)
@@ -53,11 +55,19 @@ defmodule ComponentsElixirWeb.ComponentsLive do
def handle_params(params, _uri, socket) do
search = Map.get(params, "search", "")
criteria = Map.get(params, "criteria", "name_asc")
category_id = parse_filter_id(Map.get(params, "category_id"))
storage_location_id = parse_filter_id(Map.get(params, "storage_location_id"))
# Show advanced filters if storage location is being used
show_advanced = not is_nil(storage_location_id)
{:noreply,
socket
|> assign(:search, search)
|> assign(:sort_criteria, criteria)
|> assign(:selected_category, category_id)
|> assign(:selected_storage_location, storage_location_id)
|> assign(:show_advanced_filters, show_advanced)
|> assign(:offset, 0)
|> load_components()}
end
@@ -88,21 +98,57 @@ defmodule ComponentsElixirWeb.ComponentsLive do
end
def handle_event("category_filter", %{"category_id" => ""}, socket) do
query_string = build_query_params_without_category(socket)
path = if query_string == "", do: "/", else: "/?" <> query_string
{:noreply,
socket
|> assign(:selected_category, nil)
|> assign(:offset, 0)
|> load_components()}
|> load_components()
|> push_patch(to: path)}
end
def handle_event("category_filter", %{"category_id" => category_id}, socket) do
category_id = String.to_integer(category_id)
query_string = build_query_params_with_category(socket, category_id)
path = if query_string == "", do: "/", else: "/?" <> query_string
{:noreply,
socket
|> assign(:selected_category, category_id)
|> assign(:offset, 0)
|> load_components()}
|> load_components()
|> push_patch(to: path)}
end
def handle_event("storage_location_filter", %{"storage_location_id" => ""}, socket) do
query_string = build_query_params_with_storage_location(socket, nil)
path = if query_string == "", do: "/", else: "/?" <> query_string
{:noreply,
socket
|> assign(:selected_storage_location, nil)
|> assign(:offset, 0)
|> load_components()
|> push_patch(to: path)}
end
def handle_event("storage_location_filter", %{"storage_location_id" => storage_location_id}, socket) do
storage_location_id = String.to_integer(storage_location_id)
query_string = build_query_params_with_storage_location(socket, storage_location_id)
path = if query_string == "", do: "/", else: "/?" <> query_string
{:noreply,
socket
|> assign(:selected_storage_location, storage_location_id)
|> assign(:offset, 0)
|> load_components()
|> push_patch(to: path)}
end
def handle_event("toggle_advanced_filters", _params, socket) do
{:noreply, assign(socket, :show_advanced_filters, !socket.assigns.show_advanced_filters)}
end
def handle_event("load_more", _params, socket) do
@@ -376,6 +422,7 @@ defmodule ComponentsElixirWeb.ComponentsLive do
search: socket.assigns.search,
sort_criteria: socket.assigns.sort_criteria,
category_id: socket.assigns.selected_category,
storage_location_id: socket.assigns.selected_storage_location,
limit: @items_per_page,
offset: socket.assigns.offset
]
@@ -421,7 +468,57 @@ defmodule ComponentsElixirWeb.ComponentsLive do
defp build_query_params(socket, overrides) do
params = %{
search: Map.get(overrides, :search, socket.assigns.search),
criteria: Map.get(overrides, :criteria, socket.assigns.sort_criteria)
criteria: Map.get(overrides, :criteria, socket.assigns.sort_criteria),
category_id: Map.get(overrides, :category_id, socket.assigns.selected_category),
storage_location_id: Map.get(overrides, :storage_location_id, socket.assigns.selected_storage_location)
}
params
|> Enum.reject(fn {_k, v} -> is_nil(v) or v == "" end)
|> URI.encode_query()
end
defp parse_filter_id(nil), do: nil
defp parse_filter_id(""), do: nil
defp parse_filter_id(id) when is_binary(id) do
case Integer.parse(id) do
{int_id, ""} -> int_id
_ -> nil
end
end
defp parse_filter_id(id) when is_integer(id), do: id
defp build_query_params_with_category(socket, category_id) do
params = %{
search: socket.assigns.search,
criteria: socket.assigns.sort_criteria,
category_id: category_id,
storage_location_id: socket.assigns.selected_storage_location
}
params
|> Enum.reject(fn {_k, v} -> is_nil(v) or v == "" end)
|> URI.encode_query()
end
defp build_query_params_with_storage_location(socket, storage_location_id) do
params = %{
search: socket.assigns.search,
criteria: socket.assigns.sort_criteria,
category_id: socket.assigns.selected_category,
storage_location_id: storage_location_id
}
params
|> Enum.reject(fn {_k, v} -> is_nil(v) or v == "" end)
|> URI.encode_query()
end
defp build_query_params_without_category(socket) do
params = %{
search: socket.assigns.search,
criteria: socket.assigns.sort_criteria,
storage_location_id: socket.assigns.selected_storage_location
}
params
@@ -558,7 +655,40 @@ defmodule ComponentsElixirWeb.ComponentsLive do
<% end %>
</form>
</div>
<div class="flex items-end">
<button
phx-click="toggle_advanced_filters"
class="inline-flex items-center px-3 py-2 border border-base-300 text-sm font-medium rounded-md text-base-content bg-base-100 hover:bg-base-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary"
>
<.icon name="hero-adjustments-horizontal" class="w-4 h-4 mr-2" />
<%= if @show_advanced_filters, do: "Hide", else: "More" %> Filters
</button>
</div>
</div>
<!-- Advanced Filters (Collapsible) -->
<%= if @show_advanced_filters do %>
<div class="mt-4 p-4 bg-base-100 border border-base-300 rounded-md">
<div class="flex flex-col sm:flex-row gap-4">
<div>
<label class="block text-sm font-medium text-base-content mb-2">Storage Location</label>
<form phx-change="storage_location_filter">
<select
name="storage_location_id"
class="block w-full px-3 py-2 border border-base-300 rounded-md shadow-sm bg-base-100 text-base-content focus:outline-none focus:ring-primary focus:border-primary sm:text-sm"
>
<option value="" selected={is_nil(@selected_storage_location)}>All Storage Locations</option>
<%= for {location_name, location_id} <- Hierarchical.select_options(@storage_locations, &(&1.parent)) do %>
<option value={location_id} selected={@selected_storage_location == location_id}>
{location_name}
</option>
<% end %>
</select>
</form>
</div>
</div>
</div>
<% end %>
</div>
<!-- Add Component Modal -->

View File

@@ -362,9 +362,23 @@ defmodule ComponentsElixirWeb.StorageLocationsLive do
<div class="flex items-center justify-between">
<div class="flex items-center space-x-3">
<%= if @title_tag == "h3" do %>
<h3 class={"#{@text_size} font-medium #{if @depth == 0, do: "text-base-content", else: "text-base-content/80"}"}>{@location.name}</h3>
<h3 class={"#{@text_size} font-medium"}>
<.link
navigate={~p"/?storage_location_id=#{@location.id}"}
class={"#{if @depth == 0, do: "text-base-content", else: "text-base-content/80"} hover:text-primary hover:underline"}
>
{@location.name}
</.link>
</h3>
<% else %>
<h4 class={"#{@text_size} font-medium #{if @depth == 0, do: "text-base-content", else: "text-base-content/80"}"}>{@location.name}</h4>
<h4 class={"#{@text_size} font-medium"}>
<.link
navigate={~p"/?storage_location_id=#{@location.id}"}
class={"#{if @depth == 0, do: "text-base-content", else: "text-base-content/80"} hover:text-primary hover:underline"}
>
{@location.name}
</.link>
</h4>
<% end %>
<span class="text-xs text-base-content/50">
({@count_display})
@@ -400,9 +414,23 @@ defmodule ComponentsElixirWeb.StorageLocationsLive do
<div class="flex items-start justify-between">
<div class="flex-1">
<%= if @title_tag == "h3" do %>
<h3 class={"#{@text_size} font-medium #{if @depth == 0, do: "text-base-content", else: "text-base-content/80"}"}>{@location.name}</h3>
<h3 class={"#{@text_size} font-medium"}>
<.link
navigate={~p"/?storage_location_id=#{@location.id}"}
class={"#{if @depth == 0, do: "text-base-content", else: "text-base-content/80"} hover:text-primary hover:underline"}
>
{@location.name}
</.link>
</h3>
<% else %>
<h4 class={"#{@text_size} font-medium #{if @depth == 0, do: "text-base-content", else: "text-base-content/80"}"}>{@location.name}</h4>
<h4 class={"#{@text_size} font-medium"}>
<.link
navigate={~p"/?storage_location_id=#{@location.id}"}
class={"#{if @depth == 0, do: "text-base-content", else: "text-base-content/80"} hover:text-primary hover:underline"}
>
{@location.name}
</.link>
</h4>
<% end %>
<%= if @location.description do %>
<p class="text-sm text-base-content/60 mt-1">{@location.description}</p>