diff --git a/lib/claper/accounts/user.ex b/lib/claper/accounts/user.ex index 2262642..b1484fb 100644 --- a/lib/claper/accounts/user.ex +++ b/lib/claper/accounts/user.ex @@ -3,6 +3,19 @@ defmodule Claper.Accounts.User do import Ecto.Changeset + @derive { + Flop.Schema, + max_limit: 100, + default_limit: 20, + pagination_types: [:page], + filterable: [:email, :role_id], + sortable: [:email, :role_id, :confirmed_at, :inserted_at], + default_order: %{ + order_by: [:inserted_at], + order_directions: [:desc] + } + } + @type t :: %__MODULE__{ id: integer(), uuid: Ecto.UUID.t(), diff --git a/lib/claper/admin.ex b/lib/claper/admin.ex index 96301f3..9d27f7a 100644 --- a/lib/claper/admin.ex +++ b/lib/claper/admin.ex @@ -316,128 +316,46 @@ defmodule Claper.Admin do end @doc """ - Returns a paginated list of users. + Returns a paginated list of users using Flop. - ## Options - - * `:page` - The page number (default: 1) - * `:per_page` - The number of users per page (default: 20) - * `:search` - Search term for filtering users by email - * `:role` - Filter users by role name + Accepts Flop params for filtering, sorting, and pagination. + See `Claper.Accounts.User` Flop.Schema config for available fields. ## Examples - iex> list_users_paginated(%{page: 1, per_page: 10}) - %{entries: [%User{}, ...], page_number: 1, page_size: 10, total_entries: 20, total_pages: 2} + iex> list_users_paginated(%{"page" => 1, "page_size" => 10}) + {[%User{}, ...], %Flop.Meta{}} """ def list_users_paginated(params \\ %{}) do - page = Map.get(params, "page", 1) - per_page = Map.get(params, "per_page", 20) - search = Map.get(params, "search", "") - role = Map.get(params, "role", "") - query = User |> where([u], is_nil(u.deleted_at)) |> preload(:role) + |> join(:left, [u], r in assoc(u, :role), as: :role) - query = - if search != "" do - query |> where([u], ilike(u.email, ^"%#{search}%")) - else - query - end - - query = - if role != "" do - query |> join(:inner, [u], r in assoc(u, :role), on: r.name == ^role) - else - query - end - - query = query |> order_by([u], desc: u.inserted_at) - - Repo.paginate(query, page: page, page_size: per_page) + Flop.validate_and_run!(query, params, for: User, replace_invalid_params: true) end @doc """ - Returns a paginated list of events. + Returns a paginated list of events using Flop. - ## Options - - * `:page` - The page number (default: 1) - * `:per_page` - The number of events per page (default: 20) - * `:search` - Search term for filtering events by name - * `:status` - Filter events by status (upcoming, past) - * `:start_date` - Filter events by start date - * `:end_date` - Filter events by end date - * `:creator_id` - Filter events by creator ID + Accepts Flop params for filtering, sorting, and pagination. + See `Claper.Events.Event` Flop.Schema config for available fields. ## Examples - iex> list_events_paginated(%{page: 1, per_page: 10}) - %{entries: [%Event{}, ...], page_number: 1, page_size: 10, total_entries: 20, total_pages: 2} + iex> list_events_paginated(%{"page" => 1, "page_size" => 10}) + {[%Event{}, ...], %Flop.Meta{}} """ def list_events_paginated(params \\ %{}) do - page = Map.get(params, "page", 1) - per_page = Map.get(params, "per_page", 20) - search = Map.get(params, "search", "") - status = Map.get(params, "status", "") - start_date = Map.get(params, "start_date", nil) - end_date = Map.get(params, "end_date", nil) - creator_id = Map.get(params, "creator_id", nil) - query = Event + |> join(:left, [e], u in assoc(e, :user), as: :user) |> preload(:user) - query = - if search != "" do - query |> where([e], ilike(e.name, ^"%#{search}%")) - else - query - end - - query = - case status do - "upcoming" -> - now = NaiveDateTime.utc_now() - query |> where([e], e.started_at > ^now) - - "past" -> - now = NaiveDateTime.utc_now() - query |> where([e], e.started_at <= ^now) - - _ -> - query - end - - query = - if start_date do - query |> where([e], e.started_at >= ^start_date) - else - query - end - - query = - if end_date do - query |> where([e], e.started_at <= ^end_date) - else - query - end - - query = - if creator_id do - query |> where([e], e.user_id == ^creator_id) - else - query - end - - query = query |> order_by([e], desc: e.started_at) - - Repo.paginate(query, page: page, page_size: per_page) + Flop.validate_and_run!(query, params, for: Event, replace_invalid_params: true) end @doc """ diff --git a/lib/claper/audit/log.ex b/lib/claper/audit/log.ex index fef6720..ca5e4c4 100644 --- a/lib/claper/audit/log.ex +++ b/lib/claper/audit/log.ex @@ -5,6 +5,8 @@ defmodule Claper.Audit.Log do @derive { Flop.Schema, max_limit: 100, + default_limit: 20, + pagination_types: [:page], filterable: [:action, :user_email], sortable: [:inserted_at, :action], default_order: %{ diff --git a/lib/claper/events/event.ex b/lib/claper/events/event.ex index d1f3091..59f75d4 100644 --- a/lib/claper/events/event.ex +++ b/lib/claper/events/event.ex @@ -2,6 +2,28 @@ defmodule Claper.Events.Event do use Ecto.Schema import Ecto.Changeset + @derive { + Flop.Schema, + max_limit: 100, + default_limit: 20, + pagination_types: [:page], + filterable: [:name, :code, :user_email], + sortable: [:name, :code, :started_at, :expired_at, :audience_peak], + default_order: %{ + order_by: [:started_at], + order_directions: [:desc] + }, + adapter_opts: [ + join_fields: [ + user_email: [ + binding: :user, + field: :email, + path: [:user, :email] + ] + ] + ] + } + @type t :: %__MODULE__{ id: integer(), uuid: Ecto.UUID.t(), diff --git a/lib/claper_web/components/core_components.ex b/lib/claper_web/components/core_components.ex index f3d98b8..ce1f14a 100644 --- a/lib/claper_web/components/core_components.ex +++ b/lib/claper_web/components/core_components.ex @@ -495,4 +495,84 @@ defmodule ClaperWeb.CoreComponents do def translate_errors(errors, field) when is_list(errors) do for {^field, {msg, opts}} <- errors, do: translate_error({msg, opts}) end + + @doc """ + Renders a DaisyUI pagination component from a Flop.Meta struct. + + ## Attributes + + * `meta` - A `Flop.Meta` struct with pagination info. + * `path` - The base path for pagination links. + """ + attr :meta, :map, required: true + attr :path, :string, required: true + + def pagination(assigns) do + meta = assigns.meta + + assigns = + assigns + |> assign(:current_page, meta.current_page || 1) + |> assign(:total_pages, meta.total_pages || 1) + |> assign(:has_previous?, meta.has_previous_page?) + |> assign(:has_next?, meta.has_next_page?) + |> assign(:page_range, page_range(meta.current_page || 1, meta.total_pages || 1)) + + ~H""" + 1} class="flex items-center gap-3" aria-label="Pagination"> + + {gettext("Page %{current} of %{total}", current: @current_page, total: @total_pages)} + + + <.link + patch={page_path(@path, @meta, @current_page - 1)} + class={[ + "join-item btn btn-ghost btn-sm", + !@has_previous? && "btn-disabled" + ]} + aria-label={gettext("Previous page")} + > + + + + + <.link + :for={page <- @page_range} + patch={page_path(@path, @meta, page)} + class={[ + "join-item btn btn-sm", + if(page == @current_page, do: "btn-active", else: "btn-ghost") + ]} + > + {page} + + <.link + patch={page_path(@path, @meta, @current_page + 1)} + class={[ + "join-item btn btn-ghost btn-sm", + !@has_next? && "btn-disabled" + ]} + aria-label={gettext("Next page")} + > + + + + + + + """ + end + + defp page_path(path, meta, page) do + Flop.Phoenix.build_path(path, %{meta | flop: %{meta.flop | page: page}}) + end + + defp page_range(_current, total) when total <= 5, do: 1..total + + defp page_range(current, total) do + start = max(1, current - 2) + stop = min(total, start + 4) + start = max(1, stop - 4) + start..stop + end end diff --git a/lib/claper_web/live/admin_live/audit_log_live.html.heex b/lib/claper_web/live/admin_live/audit_log_live.html.heex index b12fd30..2ded133 100644 --- a/lib/claper_web/live/admin_live/audit_log_live.html.heex +++ b/lib/claper_web/live/admin_live/audit_log_live.html.heex @@ -1,49 +1,22 @@ -