spazio-solazzo/lib/spazio_solazzo_web/live/admin/booking_management_live.ex
Víctor Martínez 69f992f8f6
feat: new booking system + admin dashboard (#12)
feat: implement a new booking system and admin dashboard
2026-02-07 19:08:39 +01:00

251 lines
7.1 KiB
Elixir

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.AdminBookingManagementComponents
alias SpazioSolazzo.BookingSystem
@pending_limit 10
@history_limit 10
def mount(_params, _session, socket) do
{:ok, spaces} = Ash.read(SpazioSolazzo.BookingSystem.Space)
if connected?(socket) do
Phoenix.PubSub.subscribe(SpazioSolazzo.PubSub, "booking:created")
Phoenix.PubSub.subscribe(SpazioSolazzo.PubSub, "booking:approved")
Phoenix.PubSub.subscribe(SpazioSolazzo.PubSub, "booking:cancelled")
Phoenix.PubSub.subscribe(SpazioSolazzo.PubSub, "booking:rejected")
end
{:ok,
assign(socket,
spaces: spaces,
expanded_booking_ids: MapSet.new(),
show_reject_modal: false,
rejecting_booking_id: nil,
rejection_reason: ""
)}
end
def handle_params(params, _uri, socket) do
# 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 =
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
MapSet.delete(socket.assigns.expanded_booking_ids, booking_id)
else
MapSet.put(socket.assigns.expanded_booking_ids, booking_id)
end
{:noreply, assign(socket, expanded_booking_ids: expanded)}
end
def handle_event("approve_booking", %{"booking_id" => booking_id}, socket) do
case Ash.get(BookingSystem.Booking, booking_id) do
{:ok, booking} ->
BookingSystem.approve_booking(booking)
{:noreply, socket}
{:error, _} ->
{:noreply, put_flash(socket, :error, "Booking not found")}
end
end
def handle_event("show_reject_modal", %{"booking_id" => booking_id}, socket) do
{:noreply,
assign(socket,
show_reject_modal: true,
rejecting_booking_id: booking_id,
rejection_reason: ""
)}
end
def handle_event("hide_reject_modal", _, socket) do
{:noreply, assign(socket, show_reject_modal: false, rejecting_booking_id: nil)}
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
%{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(BookingSystem.Booking, id) do
{:ok, booking} ->
BookingSystem.reject_booking(booking, reason)
socket =
socket
|> assign(show_reject_modal: false, rejecting_booking_id: nil)
|> put_flash(:info, "Booking rejected")
{:noreply, socket}
{:error, _} ->
{:noreply, put_flash(socket, :error, "Booking not found")}
end
end
end
def handle_info(%{topic: "booking:" <> _}, socket) do
{:noreply, fetch_bookings(socket)}
end
def handle_info(_msg, socket), do: {:noreply, socket}
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
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_data} =
BookingSystem.read_booking_history(space_id, query_email, date,
page: [limit: @history_limit, offset: history_offset, count: true],
load: [:space, :user]
)
assign(socket, pending_page: pending_data, history_page: history_data)
end
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
}
# 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)
~p"/admin/bookings"
|> URI.parse()
|> URI.append_query(URI.encode_query(params))
|> URI.to_string()
end
defp parse_page(nil), do: 1
defp parse_page(""), do: 1
defp parse_page(value) do
case Integer.parse(value) do
{n, _} when n > 0 -> n
_ -> 1
end
end
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