Skip to content
Merged
Show file tree
Hide file tree
Changes from 13 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 47 additions & 0 deletions lib/yearbook/entries.ex
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,53 @@ defmodule Yearbook.Entries do
Repo.all(Entry)
end

@doc """
Returns the list of pending entries.
"""

def list_pending_entries do
query =
from e in Entry,
where: e.status == :pending,
order_by: [desc: e.inserted_at]

Repo.all(query)
end

@doc """
Returns a paginated map of pending entries based on the provided parameters.

## Examples
iex> list_pending_entries_pagination(%{"page" => "1"})
%{entries: [%Entry{}, ...], total_pages: 3, current_page: 1}
"""
def list_pending_entries_pagination(params \\ %{}) do
Entry
|> where([e], e.status == :pending)
|> order_by([e], desc: e.inserted_at)
|> paginate(params)
end
Comment thread
deboravcaetano marked this conversation as resolved.

defp paginate(query, params) do
page = String.to_integer(params["page"] || "1")
per_page = 10

entries =
query
|> limit(^per_page)
|> offset(^((page - 1) * per_page))
|> Repo.all()

total_entries = Repo.aggregate(query, :count)
total_pages = max(1, ceil(total_entries / per_page))

%{
entries: entries,
total_pages: total_pages,
current_page: page
}
end

@doc """
Gets a single entry.

Expand Down
2 changes: 1 addition & 1 deletion lib/yearbook_web/components/navbar.ex
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ defmodule YearbookWeb.Navbar do
<%= if @current_scope && @current_scope.user do %>
<%= if @current_scope.user.role == :admin do %>
<.link
navigate={~p"/backoffice/mock"}
navigate={~p"/backoffice/approvals"}
phx-click={@is_mobile && hide_mobile_navbar()}
class="inline-flex items-center justify-center px-3 sm:w-32 h-9 sm:h-10 text-sm sm:text-base font-bold text-primary hover:opacity-80 transition cursor-pointer"
>
Expand Down
6 changes: 3 additions & 3 deletions lib/yearbook_web/config.ex
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,10 @@ defmodule YearbookWeb.Config do
def backoffice_pages do
[
%{
key: :mock,
title: "Mock",
key: :request_approvals,
title: "Pedidos",
Comment thread
deboravcaetano marked this conversation as resolved.
Outdated
Comment thread
deboravcaetano marked this conversation as resolved.
Outdated
icon: "hero-pencil",
url: "/backoffice/mock"
url: "/backoffice/approvals"
}
]
end
Expand Down
180 changes: 180 additions & 0 deletions lib/yearbook_web/live/backoffice/approvals_live/approvals.html.heex
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
<div class="flex-1 p-10 bg-white rounded-2xl shadow-sm border border-gray-100">
<div class="mb-10">
<h2 class="text-2xl font-bold text-gray-800 tracking-tight">Aprovações Pendentes</h2>
Comment thread
deboravcaetano marked this conversation as resolved.
Outdated
</div>

<div class="flex items-center px-4 pb-4 text-[11px] font-bold uppercase tracking-widest text-gray-400 border-b border-gray-100">
<div class="w-1/4">Autor</div>
<div class="hidden md:block flex-1">Frase</div>
<div class="w-32"></div>
</div>

<div class="divide-y divide-gray-100">
<%= for entry <- @entries do %>
<div class="group flex items-center px-4 py-5 hover:bg-gray-50/50 transition-colors duration-150">
<div class="w-1/4 min-w-0 pr-4">
<span class="text-sm font-semibold text-gray-700 truncate block">
{entry.name}
</span>
</div>

<div class="hidden md:block flex-1 min-w-0 pr-4">
<span class="text-sm text-gray-500 line-clamp-1 block">
{entry.text}
</span>
</div>

<div class="flex-1 md:flex-none md:w-32 flex justify-end items-center gap-4">
<button
phx-click="show_details"
phx-value-id={entry.id}
class="text-gray-600 hover:text-gray-400 transition-colors cursor-pointer"
>
<.icon name="hero-eye" class="size-5" />
</button>

<button
phx-click="confirm_action"
phx-value-id={entry.id}
phx-value-action="approve"
class="text-gray-600 hover:text-gray-400 transition-colors cursor-pointer"
>
<.icon name="hero-check" class="size-6" />
</button>

<button
phx-click="confirm_action"
phx-value-id={entry.id}
phx-value-action="deny"
class="text-gray-600 hover:text-gray-400 transition-colors cursor-pointer"
>
<.icon name="hero-trash" class="size-5" />
</button>
</div>
</div>
<% end %>
</div>

<div class="mt-7 flex items-center justify-between border-t border-gray-100 pt-3">
<span class="text-xs font-medium text-gray-400 uppercase tracking-wider">
Página <strong class="text-gray-700">{@current_page_num}</strong> de {@total_pages}
</span>

<div class="flex items-center gap-1">
<.link
:if={@current_page_num > 1}
patch={~p"/backoffice/approvals?page=#{@current_page_num - 1}"}
class="p-2 text-gray-400 hover:text-gray-700 transition-all"
>
<.icon name="hero-chevron-left" class="size-5" />
</.link>

<%= for item <- page_range(@current_page_num, @total_pages) do %>
<%= if item == :ellipsis do %>
<span class="px-2 text-gray-400">...</span>
<% else %>
<.link
patch={~p"/backoffice/approvals?page=#{item}"}
class={[
"w-8 h-8 flex items-center justify-center rounded-lg text-xs font-bold transition-all",
if(item == @current_page_num,
do: "bg-gray-800 text-white shadow-sm",
else: "text-gray-400 hover:bg-gray-100 hover:text-gray-700"
)
]}
>
{item}
</.link>
<% end %>
<% end %>

<.link
:if={@current_page_num < @total_pages}
patch={~p"/backoffice/approvals?page=#{@current_page_num + 1}"}
class="p-2 text-gray-400 hover:text-gray-700 transition-all"
>
<.icon name="hero-chevron-right" class="size-5" />
</.link>
</div>
</div>
</div>
Comment thread
deboravcaetano marked this conversation as resolved.
Outdated

<%= if @selected_entry do %>
<div class="fixed inset-0 bg-black/60 backdrop-blur-sm z-50 flex items-center justify-center p-4">
<div class="bg-white rounded-xl max-w-md w-full shadow-2xl overflow-hidden">
<div style="background-color: black" class="p-6 flex justify-between items-center">
<h3 class="text-white text-xl font-bold items-center">Detalhes do Pedido</h3>
Comment thread
deboravcaetano marked this conversation as resolved.
Outdated
<button phx-click="close_modal" class="text-white/80 hover:text-white cursor-pointer">
<.icon name="hero-x-mark" class="size-8" />
</button>
</div>

<div class="p-8">
<div class="flex flex-col items-center mb-6">
<img
src={@selected_entry.photo}
alt={@selected_entry.name}
class="w-48 h-48 rounded-xl object-cover shrink-0 border border-gray-300"
/>
</div>
<div class="mb-6">
<label class="text-xs font-bold uppercase text-gray-500 tracking-wider">Autor</label>
Comment thread
deboravcaetano marked this conversation as resolved.
Outdated
<p class="text-lg text-gray-800 font-medium">{@selected_entry.name}</p>
</div>

<div class="mb-6">
<label class="text-xs font-bold uppercase text-gray-500 tracking-wider">
Frase
Comment thread
deboravcaetano marked this conversation as resolved.
Outdated
</label>
<p class="text-gray-800 font-semibold mt-1">
"{@selected_entry.text}"
</p>
</div>
</div>
</div>
</div>
<% end %>

<%= if @confirmation do %>
<div class="fixed inset-0 bg-black/60 backdrop-blur-sm z-60 flex items-center justify-center p-4">
<div class="bg-white rounded-xl max-w-sm w-full shadow-2xl p-6 text-center">
<div class="mx-auto flex items-center justify-center h-12 w-12 rounded-full mb-4 bg-gray-100">
<.icon
name={
if @confirmation.action == "approve",
do: "hero-check",
else: "hero-exclamation-triangle"
}
class={"size-6 #{if @confirmation.action == "approve", do: "text-emerald-600", else: "text-rose-600"}"}
/>
</div>

<h3 class="text-lg font-bold text-gray-900">
{if @confirmation.action == "approve",
do: "Confirmar Aprovação",
else: "Confirmar Rejeição"}
Comment thread
deboravcaetano marked this conversation as resolved.
Outdated
</h3>
<p class="text-sm text-gray-500 mt-2">
Queres {if @confirmation.action == "approve",
do: "aprovar",
else: "rejeitar"} o pedido de <strong>{@confirmation.entry.name}</strong>?
Comment thread
deboravcaetano marked this conversation as resolved.
Outdated
</p>

<div class="mt-6 flex gap-3">
<button
phx-click="close_modal"
class="flex-1 px-4 py-2 bg-gray-100 text-gray-700 rounded-lg font-semibold hover:bg-gray-200 transition-colors cursor-pointer"
>
Cancelar
Comment thread
deboravcaetano marked this conversation as resolved.
Outdated
</button>
<button
phx-click={@confirmation.action}
phx-value-id={@confirmation.entry.id}
class={"flex-1 px-4 py-2 text-gray-700 hover:text-white rounded-lg font-semibold transition-colors cursor-pointer #{if @confirmation.action == "approve", do: "bg-emerald-100 hover:bg-emerald-300", else: "bg-red-100 hover:bg-red-300"}"}
>
Confirmar
Comment thread
deboravcaetano marked this conversation as resolved.
Outdated
</button>
</div>
</div>
</div>
<% end %>
93 changes: 93 additions & 0 deletions lib/yearbook_web/live/backoffice/approvals_live/index.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
defmodule YearbookWeb.Approvals do
@moduledoc """
Approvals page
"""
use YearbookWeb, :live_view
alias Yearbook.Entries

def mount(_params, _session, socket) do
{:ok, assign(socket, selected_entry: nil, confirmation: nil)}
end

def handle_params(params, _url, socket) do
data = Entries.list_pending_entries_pagination(params)

{:noreply,
socket
|> assign(entries: data.entries)
|> assign(current_page_num: data.current_page)
|> assign(total_pages: data.total_pages)
|> assign(params: params)
|> assign(current_page: :request_approvals)}
end

def handle_event("show_details", %{"id" => id}, socket) do
entry = Entries.get_entry!(id)
{:noreply, assign(socket, selected_entry: entry)}
end

def handle_event("confirm_action", %{"id" => id, "action" => action}, socket) do
entry = Entries.get_entry!(id)
{:noreply, assign(socket, confirmation: %{entry: entry, action: action})}
end

def handle_event("close_modal", _, socket) do
{:noreply, assign(socket, selected_entry: nil, confirmation: nil)}
end

def handle_event("approve", %{"id" => id}, socket) do
execute_update(socket, id, :accepted, "Entry approved successfully.")
end

def handle_event("deny", %{"id" => id}, socket) do
execute_update(socket, id, :denied, "Entry denied successfully.")
end

def execute_update(socket, id, status, success_msg) do
entry = Entries.get_entry!(id)

case Entries.update_entry(entry, %{status: status}) do
{:ok, _} ->
data = Entries.list_pending_entries_pagination(socket.assigns.params)

{:noreply,
socket
|> put_flash(:info, success_msg)
|> assign(confirmation: nil)
|> assign_pagination_data(data)}

{:error, _} ->
{:noreply, put_flash(socket, :error, "Erro ao processar pedido.")}
Comment thread
deboravcaetano marked this conversation as resolved.
Outdated
end
end

def assign_pagination_data(socket, data) do
assign(socket,
entries: data.entries,
current_page_num: data.current_page,
total_pages: data.total_pages
)
end

def page_range(current, total) do
if total <= 7 do
Enum.to_list(1..total)
else
([1, total] ++ Enum.to_list((current - 2)..(current + 2)))
|> Enum.filter(&(&1 >= 1 and &1 <= total))
|> Enum.uniq()
|> Enum.sort()
|> add_dots([])
end
end

def add_dots([a, b | tail], acc) when b > a + 1 do
add_dots([b | tail], acc ++ [a, :ellipsis])
end

def add_dots([head | tail], acc) do
add_dots(tail, acc ++ [head])
end

def add_dots([], acc), do: acc
end
13 changes: 0 additions & 13 deletions lib/yearbook_web/live/backoffice/mock_live/index.ex

This file was deleted.

Empty file.
2 changes: 1 addition & 1 deletion lib/yearbook_web/router.ex
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ defmodule YearbookWeb.Router do
{YearbookWeb.UserAuth, :require_authenticated},
{YearbookWeb.UserAuth, :require_admin}
] do
live "/mock", MockLive, :index
live "/approvals", Approvals, :index
end
end

Expand Down
10 changes: 10 additions & 0 deletions priv/fake/quotes.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
May the Force be with you.
It’s over, Anakin. I have the high ground.
The Avengers. That’s what we call ourselves; we’re sort of like a team. ‘Earth’s Mightiest Heroes’-type thing.
Do. Or do not. There is no try.
I’m one with the Force. The Force is with me.
Miles, being Spider-Man is a sacrifice.
You can't be a friendly neighborhood Spider-Man if there's no neighborhood.
I will never forget these words: with great power, comes great responsibility.
Anyone can wear the mask. You could wear the mask.
The idea was to bring together a group of of remarkable people to see if they could become something more. To see if they could work together when we needed them to, to fight the battles that we never could.
3 changes: 2 additions & 1 deletion priv/repo/seeds.exs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@ defmodule Yearbook.Repo.Seeds do

def run do
[
"accounts.exs"
"accounts.exs",
"entries.exs"
]
|> Enum.each(fn file ->
Code.require_file("#{@seeds_dir}/#{file}")
Expand Down
Loading
Loading