refactor booking management page to be more simple

This commit is contained in:
JasterV 2026-02-04 17:50:41 +01:00
parent ecdfc67d26
commit 386df6ade2
5 changed files with 152 additions and 263 deletions

View file

@ -1,4 +1,4 @@
defmodule SpazioSolazzoWeb.Admin.BookingManagementComponents do
defmodule SpazioSolazzoWeb.AdminBookingManagementComponents do
@moduledoc """
Reusable components for the admin booking management interface.
"""

View file

@ -1,32 +1,20 @@
defmodule SpazioSolazzoWeb.Admin.BookingManagementLive do
@moduledoc """
Admin booking management tool for reviewing and managing all booking requests.
Refactored to use URL as the Single Source of Truth.
"""
use SpazioSolazzoWeb, :live_view
import SpazioSolazzoWeb.Admin.BookingManagementComponents
import SpazioSolazzoWeb.AdminBookingManagementComponents
alias SpazioSolazzo.BookingSystem
@pending_bookings_page_limit 10
@booking_history_page_limit 10
@pending_limit 10
@history_limit 10
def mount(_params, _session, socket) do
{:ok, spaces} = Ash.read(SpazioSolazzo.BookingSystem.Space)
{:ok, pending_page} =
BookingSystem.read_pending_bookings(nil, nil, nil,
page: [limit: @pending_bookings_page_limit, offset: 0, count: true],
load: [:space, :user]
)
{:ok, history_page} =
BookingSystem.read_booking_history(nil, nil, nil,
page: [limit: @booking_history_page_limit, offset: 0, count: true],
load: [:space, :user]
)
if connected?(socket) do
Phoenix.PubSub.subscribe(SpazioSolazzo.PubSub, "booking:created")
Phoenix.PubSub.subscribe(SpazioSolazzo.PubSub, "booking:approved")
@ -37,53 +25,76 @@ defmodule SpazioSolazzoWeb.Admin.BookingManagementLive do
{:ok,
assign(socket,
spaces: spaces,
pending_page: pending_page,
history_page: history_page,
pending_page_number: 1,
history_page_number: 1,
filter_space_id: nil,
filter_email: "",
filter_date: nil,
expanded_booking_ids: MapSet.new(),
show_reject_modal: false,
rejecting_booking_id: nil,
rejection_reason: "",
expanded_booking_ids: MapSet.new()
rejection_reason: ""
)}
end
def handle_params(params, _uri, socket) do
# Parse URL parameters - URL is the source of truth for all filters
filter_space_id = if params["space_id"] == "" || is_nil(params["space_id"]), do: nil, else: params["space_id"]
filter_email = params["email"] || ""
filter_date = parse_date_param(params["date"])
pending_page = parse_page_param(params["pending_page"])
history_page = parse_page_param(params["history_page"])
# Determine if we need to reload data
needs_reload =
params_changed?(
socket.assigns.filter_space_id,
filter_space_id,
socket.assigns.filter_email,
filter_email,
socket.assigns.filter_date,
filter_date,
socket.assigns.pending_page_number,
pending_page,
socket.assigns.history_page_number,
history_page
# Destructure params with defaults. This ensures all keys exist.
%{
"space" => space_slug,
"email" => email,
"date" => date_str,
"pending_page" => pending_str,
"history_page" => history_str
} =
Map.merge(
%{
"space" => nil,
"email" => "",
"date" => nil,
"pending_page" => "1",
"history_page" => "1"
},
params
)
socket =
if needs_reload do
reload_bookings(socket, filter_space_id, filter_email, filter_date, pending_page, history_page)
else
socket
end
socket
|> assign(
filter_space: parse_string(space_slug),
filter_email: email,
filter_date: parse_date(date_str),
pending_page_number: parse_page(pending_str),
history_page_number: parse_page(history_str)
)
|> fetch_bookings()
{:noreply, socket}
end
def handle_event(
"filter_bookings",
%{"date" => date, "space" => space, "email" => email},
socket
) do
params = %{
"date" => date,
"space" => space,
"email" => email,
# Reset pagination to page 1 when filtering
"pending_page" => "1",
"history_page" => "1"
}
{:noreply, push_patch(socket, to: build_filter_path(socket, params), replace: true)}
end
def handle_event("clear_filters", _, socket) do
{:noreply, push_patch(socket, to: ~p"/admin/bookings", replace: true)}
end
def handle_event("pending_page_change", %{"page" => page}, socket) do
{:noreply, push_patch(socket, to: build_filter_path(socket, %{"pending_page" => page}))}
end
def handle_event("history_page_change", %{"page" => page}, socket) do
{:noreply, push_patch(socket, to: build_filter_path(socket, %{"history_page" => page}))}
end
def handle_event("toggle_expand", %{"booking_id" => booking_id}, socket) do
expanded =
if MapSet.member?(socket.assigns.expanded_booking_ids, booking_id) do
@ -95,77 +106,11 @@ defmodule SpazioSolazzoWeb.Admin.BookingManagementLive do
{:noreply, assign(socket, expanded_booking_ids: expanded)}
end
def handle_event("filter_bookings", params, socket) do
space_id = if params["space_id"] == "", do: nil, else: params["space_id"]
email = if params["email"] == "", do: nil, else: params["email"]
date =
if params["date"] == "",
do: nil,
else: Date.from_iso8601!(params["date"])
{:ok, pending_page} =
BookingSystem.read_pending_bookings(space_id, email, date,
page: [limit: @pending_bookings_page_limit, offset: 0, count: true],
load: [:space, :user]
)
{:ok, history_page} =
BookingSystem.read_booking_history(space_id, email, date,
page: [limit: @booking_history_page_limit, offset: 0, count: true],
load: [:space, :user]
)
updated_socket =
assign(socket,
pending_page: pending_page,
history_page: history_page,
pending_page_number: 1,
history_page_number: 1,
filter_space_id: space_id,
filter_email: email || "",
filter_date: date
)
{:noreply, push_patch(updated_socket, to: build_path(updated_socket, 1, 1))}
end
def handle_event("clear_filters", _, socket) do
{:noreply, push_patch(socket, to: ~p"/admin/bookings")}
end
def handle_event("pending_page_change", %{"page" => page_str}, socket) do
page_number = String.to_integer(page_str)
socket =
push_patch(socket,
to: build_path(socket, page_number, socket.assigns.history_page_number)
)
{:noreply, socket}
end
def handle_event("history_page_change", %{"page" => page_str}, socket) do
page_number = String.to_integer(page_str)
socket =
push_patch(socket,
to: build_path(socket, socket.assigns.pending_page_number, page_number)
)
{:noreply, socket}
end
def handle_event("approve_booking", %{"booking_id" => booking_id}, socket) do
case Ash.get(SpazioSolazzo.BookingSystem.Booking, booking_id) do
case Ash.get(BookingSystem.Booking, booking_id) do
{:ok, booking} ->
case BookingSystem.approve_booking(booking) do
{:ok, _approved} ->
refresh_bookings(socket)
{:error, _} ->
{:noreply, put_flash(socket, :error, "Failed to approve booking")}
end
BookingSystem.approve_booking(booking)
{:noreply, socket}
{:error, _} ->
{:noreply, put_flash(socket, :error, "Booking not found")}
@ -182,44 +127,31 @@ defmodule SpazioSolazzoWeb.Admin.BookingManagementLive do
end
def handle_event("hide_reject_modal", _, socket) do
{:noreply,
assign(socket,
show_reject_modal: false,
rejecting_booking_id: nil,
rejection_reason: ""
)}
{:noreply, assign(socket, show_reject_modal: false, rejecting_booking_id: nil)}
end
def handle_event("stop_propagation", _, socket) do
{:noreply, socket}
end
def handle_event("stop_propagation", _, socket), do: {:noreply, socket}
def handle_event("update_rejection_reason", %{"reason" => reason}, socket) do
{:noreply, assign(socket, rejection_reason: reason)}
end
def handle_event("confirm_reject", _, socket) do
if String.trim(socket.assigns.rejection_reason) == "" do
%{rejecting_booking_id: id, rejection_reason: reason} = socket.assigns
if String.trim(reason) == "" do
{:noreply, put_flash(socket, :error, "Please provide a rejection reason")}
else
case Ash.get(SpazioSolazzo.BookingSystem.Booking, socket.assigns.rejecting_booking_id) do
case Ash.get(BookingSystem.Booking, id) do
{:ok, booking} ->
case BookingSystem.reject_booking(booking, socket.assigns.rejection_reason) do
{:ok, _rejected} ->
socket =
socket
|> assign(
show_reject_modal: false,
rejecting_booking_id: nil,
rejection_reason: ""
)
|> put_flash(:info, "Booking rejected")
BookingSystem.reject_booking(booking, reason)
refresh_bookings(socket)
socket =
socket
|> assign(show_reject_modal: false, rejecting_booking_id: nil)
|> put_flash(:info, "Booking rejected")
{:error, _} ->
{:noreply, put_flash(socket, :error, "Failed to reject booking")}
end
{:noreply, socket}
{:error, _} ->
{:noreply, put_flash(socket, :error, "Booking not found")}
@ -227,136 +159,93 @@ defmodule SpazioSolazzoWeb.Admin.BookingManagementLive do
end
end
def handle_info(
%{topic: "booking:" <> _event},
socket
) do
refresh_bookings(socket)
def handle_info(%{topic: "booking:" <> _}, socket) do
{:noreply, fetch_bookings(socket)}
end
def handle_info(_msg, socket) do
{:noreply, socket}
end
def handle_info(_msg, socket), do: {:noreply, socket}
defp refresh_bookings(socket) do
pending_page_number = socket.assigns.pending_page_number
history_page_number = socket.assigns.history_page_number
pending_offset = (pending_page_number - 1) * @pending_bookings_page_limit
history_offset = (history_page_number - 1) * @booking_history_page_limit
defp fetch_bookings(socket) do
%{
filter_space: space_slug,
filter_email: email,
filter_date: date,
pending_page_number: pending_page,
history_page_number: history_page,
spaces: spaces
} = socket.assigns
{:ok, pending_page} =
BookingSystem.read_pending_bookings(
socket.assigns.filter_space_id,
socket.assigns.filter_email,
socket.assigns.filter_date,
page: [limit: @pending_bookings_page_limit, offset: pending_offset, count: true],
space_id =
spaces
|> Enum.find(%{}, fn s -> s.slug == space_slug end)
|> Map.get(:id, nil)
query_email = parse_string(email)
pending_offset = (pending_page - 1) * @pending_limit
history_offset = (history_page - 1) * @history_limit
{:ok, pending_data} =
BookingSystem.read_pending_bookings(space_id, query_email, date,
page: [limit: @pending_limit, offset: pending_offset, count: true],
load: [:space, :user]
)
{:ok, history_page} =
BookingSystem.read_booking_history(
socket.assigns.filter_space_id,
socket.assigns.filter_email,
socket.assigns.filter_date,
page: [limit: @booking_history_page_limit, offset: history_offset, count: true],
{:ok, history_data} =
BookingSystem.read_booking_history(space_id, query_email, date,
page: [limit: @history_limit, offset: history_offset, count: true],
load: [:space, :user]
)
{:noreply,
assign(socket,
pending_page: pending_page,
history_page: history_page
)}
assign(socket, pending_page: pending_data, history_page: history_data)
end
defp reload_bookings(socket, filter_space_id, filter_email, filter_date, pending_page_number, history_page_number) do
pending_offset = (pending_page_number - 1) * @pending_bookings_page_limit
history_offset = (history_page_number - 1) * @booking_history_page_limit
defp build_filter_path(socket, overrides) do
current = %{
"space" => socket.assigns.filter_space,
"email" => socket.assigns.filter_email,
"date" => socket.assigns.filter_date,
"pending_page" => socket.assigns.pending_page_number,
"history_page" => socket.assigns.history_page_number
}
email = if filter_email == "", do: nil, else: filter_email
# Merge overrides -> Filter empty values -> Encode
params =
current
|> Map.merge(overrides)
|> Enum.reduce(%{}, fn
{_k, nil}, acc -> acc
{_k, ""}, acc -> acc
{"date", %Date{} = d}, acc -> Map.put(acc, "date", Date.to_iso8601(d))
{k, v}, acc -> Map.put(acc, k, v)
end)
{:ok, pending_page} =
BookingSystem.read_pending_bookings(filter_space_id, email, filter_date,
page: [limit: @pending_bookings_page_limit, offset: pending_offset, count: true],
load: [:space, :user]
)
{:ok, history_page} =
BookingSystem.read_booking_history(filter_space_id, email, filter_date,
page: [limit: @booking_history_page_limit, offset: history_offset, count: true],
load: [:space, :user]
)
assign(socket,
pending_page: pending_page,
history_page: history_page,
pending_page_number: pending_page_number,
history_page_number: history_page_number,
filter_space_id: filter_space_id,
filter_email: filter_email,
filter_date: filter_date
)
~p"/admin/bookings"
|> URI.parse()
|> URI.append_query(URI.encode_query(params))
|> URI.to_string()
end
defp build_path(socket, pending_page, history_page) do
query_params = []
defp parse_page(nil), do: 1
defp parse_page(""), do: 1
query_params =
if pending_page != 1,
do: [{"pending_page", pending_page} | query_params],
else: query_params
query_params =
if history_page != 1,
do: [{"history_page", history_page} | query_params],
else: query_params
query_params =
if socket.assigns.filter_space_id,
do: [{"space_id", socket.assigns.filter_space_id} | query_params],
else: query_params
query_params =
if socket.assigns.filter_email && socket.assigns.filter_email != "",
do: [{"email", socket.assigns.filter_email} | query_params],
else: query_params
query_params =
if socket.assigns.filter_date,
do: [{"date", Date.to_iso8601(socket.assigns.filter_date)} | query_params],
else: query_params
base_path = ~p"/admin/bookings"
if query_params == [] do
base_path
else
query_string = URI.encode_query(query_params)
"#{base_path}?#{query_string}"
end
end
defp parse_date_param(nil), do: nil
defp parse_date_param(""), do: nil
defp parse_date_param(date_string) do
case Date.from_iso8601(date_string) do
{:ok, date} -> date
{:error, _} -> nil
end
end
defp parse_page_param(nil), do: 1
defp parse_page_param(""), do: 1
defp parse_page_param(page_string) do
case Integer.parse(page_string) do
{page, _} when page > 0 -> page
defp parse_page(value) do
case Integer.parse(value) do
{n, _} when n > 0 -> n
_ -> 1
end
end
defp params_changed?(old_space_id, new_space_id, old_email, new_email, old_date, new_date, old_pending, new_pending, old_history, new_history) do
old_space_id != new_space_id or old_email != new_email or old_date != new_date or old_pending != new_pending or old_history != new_history
defp parse_date(nil), do: nil
defp parse_date(""), do: nil
defp parse_date(value) do
case Date.from_iso8601(value) do
{:ok, date} -> date
_ -> nil
end
end
defp parse_string(nil), do: nil
defp parse_string(""), do: nil
defp parse_string(value), do: value
end

View file

@ -60,12 +60,12 @@
<.icon name="hero-map-pin" class="w-5 h-5" />
</span>
<select
name="space_id"
name="space"
class="w-full h-12 pl-10 pr-10 rounded-xl border border-slate-300 dark:border-slate-600 bg-white dark:bg-slate-700 text-slate-900 dark:text-white focus:ring-2 focus:ring-primary focus:border-transparent outline-none appearance-none cursor-pointer"
>
<option value="">All Spaces</option>
<%= for space <- @spaces do %>
<option value={space.id} selected={@filter_space_id == space.id}>
<option value={space.slug} selected={@filter_space == space.slug}>
{space.name}
</option>
<% end %>
@ -97,7 +97,7 @@
<button
type="button"
phx-click="clear_filters"
class="w-full h-12 bg-slate-900 dark:bg-white hover:bg-slate-800 dark:hover:bg-slate-100 text-white dark:text-slate-900 font-bold rounded-xl transition-colors flex items-center justify-center gap-2"
class="w-full h-12 bg-slate-900 dark:bg-white hover:bg-slate-800 dark:hover:bg-slate-100 text-white dark:text-slate-900 font-bold rounded-xl transition-colors flex items-center justify-center gap-2 cursor-pointer"
>
<span>Clear Filters</span>
<.icon name="hero-x-mark" class="w-4 h-4" />

View file

@ -233,7 +233,7 @@
<.button
type="submit"
form="customer-details-form"
class="w-full sm:w-auto flex items-center justify-center gap-2 overflow-hidden rounded-xl h-12 px-10 bg-primary hover:bg-primary/90 transition-colors text-white text-base font-bold shadow-lg shadow-primary/30"
class="w-full sm:w-auto flex items-center justify-center gap-2 overflow-hidden rounded-xl h-12 px-10 bg-primary hover:bg-primary/90 transition-colors text-white text-base font-bold shadow-lg shadow-primary/30 cursor-pointer"
>
<span>Create Booking</span>
</.button>

View file

@ -136,7 +136,7 @@ defmodule SpazioSolazzoWeb.Admin.BookingManagementPaginationTest do
|> element("button[phx-click='pending_page_change']", "2")
|> render_click()
assert_patch(view, "/admin/bookings?pending_page=2")
assert_patch(view, "/admin/bookings?history_page=1&pending_page=2")
end
test "filters reset pagination to page 1", %{
@ -166,7 +166,7 @@ defmodule SpazioSolazzoWeb.Admin.BookingManagementPaginationTest do
end
conn = log_in_user(conn, admin_user)
{:ok, view, _html} = live(conn, "/admin/bookings?pending_page=2")
{:ok, view, _html} = live(conn, "/admin/bookings?history_page=1&pending_page=2")
view
|> form("form", %{"email" => "customer1@example.com"})
@ -305,7 +305,7 @@ defmodule SpazioSolazzoWeb.Admin.BookingManagementPaginationTest do
|> element("button[phx-click='history_page_change']", "2")
|> render_click()
assert_patch(view, "/admin/bookings?history_page=2")
assert_patch(view, "/admin/bookings?history_page=2&pending_page=1")
end
end
@ -461,7 +461,7 @@ defmodule SpazioSolazzoWeb.Admin.BookingManagementPaginationTest do
{:ok, view, _html} = live(conn, "/admin/bookings")
view
|> form("form", %{"space_id" => space.id})
|> form("form", %{"space" => space.slug})
|> render_change()
html = render(view)