mirror of
https://codeberg.org/JasterV/spazio-solazzo.git
synced 2026-04-26 18:20:03 +00:00
feat: add pagination to adming booking management site
This commit is contained in:
parent
3239ef0bd9
commit
ecdfc67d26
12 changed files with 1665 additions and 546 deletions
|
|
@ -26,8 +26,12 @@ defmodule SpazioSolazzo.BookingSystem do
|
|||
end
|
||||
|
||||
resource SpazioSolazzo.BookingSystem.Booking do
|
||||
define :admin_search_bookings,
|
||||
action: :admin_dashboard_search,
|
||||
define :read_pending_bookings,
|
||||
action: :read_pending_bookings,
|
||||
args: [:space_id, :email, :date]
|
||||
|
||||
define :read_booking_history,
|
||||
action: :read_booking_history,
|
||||
args: [:space_id, :email, :date]
|
||||
|
||||
define :search_bookings,
|
||||
|
|
|
|||
|
|
@ -97,38 +97,55 @@ defmodule SpazioSolazzo.BookingSystem.Booking do
|
|||
end
|
||||
end
|
||||
|
||||
read :admin_dashboard_search do
|
||||
description "Search query tailored for the admin booking management panel"
|
||||
read :read_pending_bookings do
|
||||
description "Fetch pending bookings for admin dashboard with pagination"
|
||||
|
||||
argument :space_id, :uuid, allow_nil?: true
|
||||
argument :email, :string, allow_nil?: true
|
||||
argument :date, :date, allow_nil?: true
|
||||
|
||||
filter expr(state == :requested or state == :accepted)
|
||||
# Only requested bookings
|
||||
filter expr(state == :requested)
|
||||
|
||||
pagination do
|
||||
required? false
|
||||
offset? true
|
||||
countable true
|
||||
default_limit 10
|
||||
max_page_size 50
|
||||
end
|
||||
|
||||
# Apply shared admin filters preparation
|
||||
prepare SpazioSolazzo.BookingSystem.Preparations.ApplyAdminFilters
|
||||
|
||||
prepare fn query, _ctx ->
|
||||
query =
|
||||
case Ash.Query.get_argument(query, :space_id) do
|
||||
nil -> query
|
||||
space_id -> Ash.Query.filter(query, space_id == ^space_id)
|
||||
end
|
||||
Ash.Query.sort(query, inserted_at: :desc)
|
||||
end
|
||||
end
|
||||
|
||||
query =
|
||||
case Ash.Query.get_argument(query, :email) do
|
||||
nil -> query
|
||||
email -> Ash.Query.filter(query, customer_email == ^email)
|
||||
end
|
||||
read :read_booking_history do
|
||||
description "Fetch historical bookings (accepted/rejected/cancelled) with pagination"
|
||||
|
||||
case Ash.Query.get_argument(query, :date) do
|
||||
nil ->
|
||||
query
|
||||
argument :space_id, :uuid, allow_nil?: true
|
||||
argument :email, :string, allow_nil?: true
|
||||
argument :date, :date, allow_nil?: true
|
||||
|
||||
date ->
|
||||
day_start = DateTime.new!(date, ~T[00:00:00], "Etc/UTC")
|
||||
day_end = DateTime.new!(date, ~T[23:59:59], "Etc/UTC")
|
||||
# Non-pending states
|
||||
filter expr(state in [:accepted, :rejected, :cancelled])
|
||||
|
||||
Ash.Query.filter(query, start_datetime < ^day_end and end_datetime > ^day_start)
|
||||
end
|
||||
pagination do
|
||||
required? false
|
||||
offset? true
|
||||
countable true
|
||||
default_limit 25
|
||||
max_page_size 100
|
||||
end
|
||||
|
||||
# Apply shared admin filters preparation
|
||||
prepare SpazioSolazzo.BookingSystem.Preparations.ApplyAdminFilters
|
||||
|
||||
prepare fn query, _ctx ->
|
||||
Ash.Query.sort(query, start_datetime: :desc)
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,40 @@
|
|||
defmodule SpazioSolazzo.BookingSystem.Preparations.ApplyAdminFilters do
|
||||
@moduledoc """
|
||||
Ash Preparation that applies common admin filters (space_id, email, date) to booking queries.
|
||||
"""
|
||||
use Ash.Resource.Preparation
|
||||
|
||||
@impl true
|
||||
def prepare(query, _opts, _context) do
|
||||
query
|
||||
|> apply_space_filter()
|
||||
|> apply_email_filter()
|
||||
|> apply_date_filter()
|
||||
end
|
||||
|
||||
defp apply_space_filter(query) do
|
||||
case Ash.Query.get_argument(query, :space_id) do
|
||||
nil -> query
|
||||
space_id -> Ash.Query.filter(query, space_id == ^space_id)
|
||||
end
|
||||
end
|
||||
|
||||
defp apply_email_filter(query) do
|
||||
case Ash.Query.get_argument(query, :email) do
|
||||
nil -> query
|
||||
email -> Ash.Query.filter(query, customer_email == ^email)
|
||||
end
|
||||
end
|
||||
|
||||
defp apply_date_filter(query) do
|
||||
case Ash.Query.get_argument(query, :date) do
|
||||
nil ->
|
||||
query
|
||||
|
||||
date ->
|
||||
day_start = DateTime.new!(date, ~T[00:00:00], "Etc/UTC")
|
||||
day_end = DateTime.new!(date, ~T[23:59:59], "Etc/UTC")
|
||||
Ash.Query.filter(query, start_datetime < ^day_end and end_datetime > ^day_start)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,233 @@
|
|||
defmodule SpazioSolazzoWeb.Admin.BookingManagementComponents do
|
||||
@moduledoc """
|
||||
Reusable components for the admin booking management interface.
|
||||
"""
|
||||
use Phoenix.Component
|
||||
import SpazioSolazzoWeb.CoreComponents
|
||||
|
||||
attr :title, :string, required: true
|
||||
attr :bookings, :list, required: true
|
||||
attr :page, :map, required: true
|
||||
attr :current_page, :integer, required: true
|
||||
attr :event_prefix, :string, required: true
|
||||
attr :expanded_booking_ids, :any, required: true
|
||||
attr :show_actions, :boolean, default: false
|
||||
attr :show_cancellation_details, :boolean, default: false
|
||||
|
||||
def bookings_table(assigns) do
|
||||
~H"""
|
||||
<div>
|
||||
<h2 class="text-xl font-bold text-slate-900 dark:text-white mb-4">{@title}</h2>
|
||||
<div class="bg-white dark:bg-slate-800 rounded-2xl border border-slate-200 dark:border-slate-700 shadow-sm overflow-hidden">
|
||||
<div class="overflow-x-auto">
|
||||
<table class="min-w-full divide-y divide-slate-200 dark:divide-slate-700">
|
||||
<thead class="bg-slate-50 dark:bg-slate-900">
|
||||
<tr>
|
||||
<th class="px-6 py-3 text-left text-xs font-bold text-slate-600 dark:text-slate-400 uppercase tracking-wider w-[4%]">
|
||||
</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-bold text-slate-600 dark:text-slate-400 uppercase tracking-wider">
|
||||
Space
|
||||
</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-bold text-slate-600 dark:text-slate-400 uppercase tracking-wider">
|
||||
Start
|
||||
</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-bold text-slate-600 dark:text-slate-400 uppercase tracking-wider">
|
||||
End
|
||||
</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-bold text-slate-600 dark:text-slate-400 uppercase tracking-wider">
|
||||
Customer
|
||||
</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-bold text-slate-600 dark:text-slate-400 uppercase tracking-wider">
|
||||
Status
|
||||
</th>
|
||||
<%= if @show_actions do %>
|
||||
<th class="px-6 py-3 text-center text-xs font-bold text-slate-600 dark:text-slate-400 uppercase tracking-wider min-w-[240px]">
|
||||
Actions
|
||||
</th>
|
||||
<% end %>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-slate-200 dark:divide-slate-700">
|
||||
<%= for booking <- @bookings do %>
|
||||
<% is_expanded = MapSet.member?(@expanded_booking_ids, booking.id) %>
|
||||
<tr class={["group", if(is_expanded, do: "expanded", else: "")]}>
|
||||
<td class="px-3 py-4 whitespace-nowrap align-top">
|
||||
<button
|
||||
phx-click="toggle_expand"
|
||||
phx-value-booking_id={booking.id}
|
||||
class="flex items-center justify-center size-7 rounded-full text-slate-500 hover:bg-slate-100 dark:hover:bg-slate-700 transition-colors"
|
||||
>
|
||||
<.icon
|
||||
name="hero-chevron-down"
|
||||
class={[
|
||||
"w-4 h-4 transition-transform",
|
||||
if(is_expanded, do: "rotate-180", else: "")
|
||||
]}
|
||||
/>
|
||||
</button>
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="size-8 rounded-full bg-indigo-50 dark:bg-indigo-900/30 text-indigo-600 dark:text-indigo-400 flex items-center justify-center">
|
||||
<.icon name="hero-building-office" class="w-4 h-4" />
|
||||
</div>
|
||||
<div>
|
||||
<p class="font-medium text-slate-900 dark:text-white">
|
||||
{booking.space.name}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap">
|
||||
<p class="text-sm text-slate-900 dark:text-slate-200">
|
||||
{SpazioSolazzo.CalendarExt.format_datetime_range_start(booking.start_datetime)}
|
||||
</p>
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap">
|
||||
<p class="text-sm text-slate-900 dark:text-slate-200">
|
||||
{SpazioSolazzo.CalendarExt.format_datetime_range_end(
|
||||
booking.start_datetime,
|
||||
booking.end_datetime
|
||||
)}
|
||||
</p>
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap">
|
||||
<div>
|
||||
<p class="font-medium text-slate-900 dark:text-white">
|
||||
{booking.customer_name}
|
||||
</p>
|
||||
<p class="text-xs text-slate-600 dark:text-slate-400">
|
||||
{booking.customer_email}
|
||||
</p>
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap">
|
||||
<span class={[
|
||||
status_badge_classes(booking.state),
|
||||
"text-xs font-bold px-3 py-1 rounded-full flex items-center gap-1 w-fit"
|
||||
]}>
|
||||
<.icon name={status_icon(booking.state)} class="w-3.5 h-3.5" />
|
||||
{status_label(booking.state)}
|
||||
</span>
|
||||
</td>
|
||||
<%= if @show_actions do %>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-center">
|
||||
<div class="flex justify-center gap-3">
|
||||
<button
|
||||
phx-click="show_reject_modal"
|
||||
phx-value-booking_id={booking.id}
|
||||
class="flex items-center justify-center px-4 py-2 rounded-lg border border-red-200 dark:border-red-900 text-red-600 dark:text-red-400 bg-white dark:bg-transparent hover:bg-red-50 dark:hover:bg-red-900/20 font-bold text-sm transition-colors"
|
||||
>
|
||||
Reject
|
||||
</button>
|
||||
<button
|
||||
phx-click="approve_booking"
|
||||
phx-value-booking_id={booking.id}
|
||||
class="flex items-center justify-center px-4 py-2 rounded-lg bg-primary hover:bg-primary-hover text-white font-bold text-sm transition-colors shadow-sm"
|
||||
>
|
||||
Confirm
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
<% end %>
|
||||
</tr>
|
||||
<%= if is_expanded do %>
|
||||
<tr class="bg-slate-50 dark:bg-slate-900/50">
|
||||
<td class="px-3 py-2"></td>
|
||||
<td
|
||||
class="px-6 py-4 text-sm text-slate-600 dark:text-slate-400"
|
||||
colspan={if @show_actions, do: "6", else: "5"}
|
||||
>
|
||||
<div class="flex flex-col gap-2">
|
||||
<p>
|
||||
<strong class="font-semibold text-slate-900 dark:text-white">
|
||||
Phone:
|
||||
</strong>
|
||||
<%= if booking.customer_phone do %>
|
||||
{booking.customer_phone}
|
||||
<% else %>
|
||||
<span class="italic text-slate-400">Not provided</span>
|
||||
<% end %>
|
||||
</p>
|
||||
<p>
|
||||
<strong class="font-semibold text-slate-900 dark:text-white">
|
||||
Note:
|
||||
</strong>
|
||||
<%= if booking.customer_comment do %>
|
||||
{booking.customer_comment}
|
||||
<% else %>
|
||||
<span class="italic text-slate-400">Not provided</span>
|
||||
<% end %>
|
||||
</p>
|
||||
<%= if @show_cancellation_details && booking.state == :rejected do %>
|
||||
<p>
|
||||
<strong class="font-semibold text-slate-900 dark:text-white">
|
||||
Rejection Reason:
|
||||
</strong>
|
||||
<%= if booking.rejection_reason do %>
|
||||
{booking.rejection_reason}
|
||||
<% else %>
|
||||
<span class="italic text-slate-400">Not provided</span>
|
||||
<% end %>
|
||||
</p>
|
||||
<% end %>
|
||||
<%= if @show_cancellation_details && booking.state == :cancelled do %>
|
||||
<p>
|
||||
<strong class="font-semibold text-slate-900 dark:text-white">
|
||||
Cancellation Reason:
|
||||
</strong>
|
||||
<%= if booking.cancellation_reason do %>
|
||||
{booking.cancellation_reason}
|
||||
<% else %>
|
||||
<span class="italic text-slate-400">Not provided</span>
|
||||
<% end %>
|
||||
</p>
|
||||
<% end %>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<% end %>
|
||||
<% end %>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<.pagination_controls
|
||||
page={@page}
|
||||
current_page={@current_page}
|
||||
event_prefix={@event_prefix}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
"""
|
||||
end
|
||||
|
||||
defp status_badge_classes(:requested) do
|
||||
"bg-amber-100 dark:bg-amber-900/40 text-amber-800 dark:text-amber-200"
|
||||
end
|
||||
|
||||
defp status_badge_classes(:accepted) do
|
||||
"bg-green-100 dark:bg-green-900/40 text-green-800 dark:text-green-200"
|
||||
end
|
||||
|
||||
defp status_badge_classes(:rejected) do
|
||||
"bg-red-100 dark:bg-red-900/40 text-red-800 dark:text-red-200"
|
||||
end
|
||||
|
||||
defp status_badge_classes(:cancelled) do
|
||||
"bg-slate-100 dark:bg-slate-900/40 text-slate-800 dark:text-slate-200"
|
||||
end
|
||||
|
||||
defp status_badge_classes(_), do: "bg-slate-100 text-slate-800"
|
||||
|
||||
defp status_icon(:requested), do: "hero-clock"
|
||||
defp status_icon(:accepted), do: "hero-check-circle"
|
||||
defp status_icon(:rejected), do: "hero-x-circle"
|
||||
defp status_icon(:cancelled), do: "hero-minus-circle"
|
||||
defp status_icon(_), do: "hero-question-mark-circle"
|
||||
|
||||
defp status_label(:requested), do: "Pending"
|
||||
defp status_label(:accepted), do: "Confirmed"
|
||||
defp status_label(:rejected), do: "Rejected"
|
||||
defp status_label(:cancelled), do: "Cancelled"
|
||||
defp status_label(_), do: "Unknown"
|
||||
end
|
||||
|
|
@ -671,6 +671,123 @@ defmodule SpazioSolazzoWeb.CoreComponents do
|
|||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Renders pagination controls for Ash offset pagination.
|
||||
|
||||
## Examples
|
||||
|
||||
<.pagination_controls
|
||||
page={@page}
|
||||
current_page={@current_page_number}
|
||||
event_prefix="bookings"
|
||||
/>
|
||||
"""
|
||||
attr :page, :map, required: true, doc: "The Ash.Page.Offset struct"
|
||||
attr :current_page, :integer, required: true, doc: "Current page number (1-indexed)"
|
||||
attr :event_prefix, :string, required: true, doc: "Prefix for phx-click event"
|
||||
|
||||
def pagination_controls(assigns) do
|
||||
total_count = assigns.page.count || 0
|
||||
limit = assigns.page.limit || 10
|
||||
current = assigns.current_page
|
||||
|
||||
start_item = if total_count == 0, do: 0, else: (current - 1) * limit + 1
|
||||
end_item = min(current * limit, total_count)
|
||||
total_pages = if limit > 0, do: ceil(total_count / limit), else: 1
|
||||
|
||||
# Calculate visible pages (show max 7 with ellipsis)
|
||||
visible_pages =
|
||||
cond do
|
||||
total_pages <= 7 ->
|
||||
Enum.to_list(1..total_pages)
|
||||
|
||||
current <= 4 ->
|
||||
[1, 2, 3, 4, 5, :ellipsis, total_pages]
|
||||
|
||||
current >= total_pages - 3 ->
|
||||
[1, :ellipsis | Enum.to_list((total_pages - 4)..total_pages)]
|
||||
|
||||
true ->
|
||||
[1, :ellipsis, current - 1, current, current + 1, :ellipsis, total_pages]
|
||||
end
|
||||
|
||||
assigns =
|
||||
assign(assigns,
|
||||
total_count: total_count,
|
||||
start_item: start_item,
|
||||
end_item: end_item,
|
||||
total_pages: total_pages,
|
||||
visible_pages: visible_pages
|
||||
)
|
||||
|
||||
~H"""
|
||||
<div class="px-6 py-4 bg-slate-50 dark:bg-slate-900 border-t border-slate-200 dark:border-slate-700">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="text-sm text-slate-600 dark:text-slate-400">
|
||||
Showing {@start_item}-{@end_item} of {@total_count}
|
||||
</div>
|
||||
|
||||
<%= if @total_pages > 1 do %>
|
||||
<div class="flex items-center gap-2">
|
||||
<button
|
||||
phx-click={"#{@event_prefix}_page_change"}
|
||||
phx-value-page={@current_page - 1}
|
||||
disabled={@current_page == 1}
|
||||
class={[
|
||||
"px-3 py-1.5 rounded-lg font-medium text-sm transition-colors",
|
||||
if(@current_page == 1,
|
||||
do: "bg-slate-200 dark:bg-slate-700 text-slate-400 cursor-not-allowed",
|
||||
else:
|
||||
"bg-white dark:bg-slate-800 text-slate-700 dark:text-slate-200 border border-slate-300 dark:border-slate-600 hover:bg-slate-50"
|
||||
)
|
||||
]}
|
||||
>
|
||||
<.icon name="hero-chevron-left" class="w-4 h-4" />
|
||||
</button>
|
||||
|
||||
<%= for page_num <- @visible_pages do %>
|
||||
<%= if page_num == :ellipsis do %>
|
||||
<span class="px-2 text-slate-400">...</span>
|
||||
<% else %>
|
||||
<button
|
||||
phx-click={"#{@event_prefix}_page_change"}
|
||||
phx-value-page={page_num}
|
||||
class={[
|
||||
"px-3 py-1.5 rounded-lg font-medium text-sm transition-colors",
|
||||
if(page_num == @current_page,
|
||||
do: "bg-primary text-white",
|
||||
else:
|
||||
"bg-white dark:bg-slate-800 text-slate-700 dark:text-slate-200 border border-slate-300 dark:border-slate-600 hover:bg-slate-50"
|
||||
)
|
||||
]}
|
||||
>
|
||||
{page_num}
|
||||
</button>
|
||||
<% end %>
|
||||
<% end %>
|
||||
|
||||
<button
|
||||
phx-click={"#{@event_prefix}_page_change"}
|
||||
phx-value-page={@current_page + 1}
|
||||
disabled={@current_page == @total_pages}
|
||||
class={[
|
||||
"px-3 py-1.5 rounded-lg font-medium text-sm transition-colors",
|
||||
if(@current_page == @total_pages,
|
||||
do: "bg-slate-200 dark:bg-slate-700 text-slate-400 cursor-not-allowed",
|
||||
else:
|
||||
"bg-white dark:bg-slate-800 text-slate-700 dark:text-slate-200 border border-slate-300 dark:border-slate-600 hover:bg-slate-50"
|
||||
)
|
||||
]}
|
||||
>
|
||||
<.icon name="hero-chevron-right" class="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
"""
|
||||
end
|
||||
|
||||
@doc """
|
||||
Translates the errors for a field from a keyword list of errors.
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -5,14 +5,27 @@ defmodule SpazioSolazzoWeb.Admin.BookingManagementLive do
|
|||
|
||||
use SpazioSolazzoWeb, :live_view
|
||||
|
||||
import SpazioSolazzoWeb.Admin.BookingManagementComponents
|
||||
|
||||
alias SpazioSolazzo.BookingSystem
|
||||
|
||||
@pending_bookings_page_limit 10
|
||||
@booking_history_page_limit 10
|
||||
|
||||
def mount(_params, _session, socket) do
|
||||
{:ok, spaces} = Ash.read(SpazioSolazzo.BookingSystem.Space)
|
||||
{:ok, bookings} = BookingSystem.admin_search_bookings(nil, nil, nil, load: [:space, :user])
|
||||
|
||||
# Separate pending and other bookings
|
||||
{pending, past} = Enum.split_with(bookings, &(&1.state == :requested))
|
||||
{: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")
|
||||
|
|
@ -24,8 +37,10 @@ defmodule SpazioSolazzoWeb.Admin.BookingManagementLive do
|
|||
{:ok,
|
||||
assign(socket,
|
||||
spaces: spaces,
|
||||
pending_bookings: pending,
|
||||
past_bookings: past,
|
||||
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,
|
||||
|
|
@ -37,26 +52,31 @@ defmodule SpazioSolazzoWeb.Admin.BookingManagementLive do
|
|||
end
|
||||
|
||||
def handle_params(params, _uri, socket) do
|
||||
# Parse date from URL if present
|
||||
filter_date =
|
||||
case Map.get(params, "date") do
|
||||
nil ->
|
||||
nil
|
||||
# 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"])
|
||||
|
||||
"" ->
|
||||
nil
|
||||
# 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
|
||||
)
|
||||
|
||||
date_string ->
|
||||
case Date.from_iso8601(date_string) do
|
||||
{:ok, date} -> date
|
||||
{:error, _} -> nil
|
||||
end
|
||||
end
|
||||
|
||||
# If we have a date from URL, apply it to filters
|
||||
socket =
|
||||
if filter_date && socket.assigns.filter_date != filter_date do
|
||||
apply_date_filter(socket, filter_date)
|
||||
if needs_reload do
|
||||
reload_bookings(socket, filter_space_id, filter_email, filter_date, pending_page, history_page)
|
||||
else
|
||||
socket
|
||||
end
|
||||
|
|
@ -84,33 +104,56 @@ defmodule SpazioSolazzoWeb.Admin.BookingManagementLive do
|
|||
do: nil,
|
||||
else: Date.from_iso8601!(params["date"])
|
||||
|
||||
{:ok, bookings} =
|
||||
BookingSystem.admin_search_bookings(space_id, email, date, load: [:space, :user])
|
||||
{:ok, pending_page} =
|
||||
BookingSystem.read_pending_bookings(space_id, email, date,
|
||||
page: [limit: @pending_bookings_page_limit, offset: 0, count: true],
|
||||
load: [:space, :user]
|
||||
)
|
||||
|
||||
{pending, past} = Enum.split_with(bookings, &(&1.state == :requested))
|
||||
{:ok, history_page} =
|
||||
BookingSystem.read_booking_history(space_id, email, date,
|
||||
page: [limit: @booking_history_page_limit, offset: 0, count: true],
|
||||
load: [:space, :user]
|
||||
)
|
||||
|
||||
{:noreply,
|
||||
assign(socket,
|
||||
pending_bookings: pending,
|
||||
past_bookings: past,
|
||||
filter_space_id: space_id,
|
||||
filter_email: email || "",
|
||||
filter_date: date
|
||||
)}
|
||||
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
|
||||
{:ok, bookings} = BookingSystem.admin_search_bookings(nil, nil, nil, load: [:space, :user])
|
||||
{pending, past} = Enum.split_with(bookings, &(&1.state == :requested))
|
||||
{:noreply, push_patch(socket, to: ~p"/admin/bookings")}
|
||||
end
|
||||
|
||||
{:noreply,
|
||||
assign(socket,
|
||||
pending_bookings: pending,
|
||||
past_bookings: past,
|
||||
filter_space_id: nil,
|
||||
filter_email: "",
|
||||
filter_date: nil
|
||||
)}
|
||||
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
|
||||
|
|
@ -196,66 +239,124 @@ defmodule SpazioSolazzoWeb.Admin.BookingManagementLive do
|
|||
end
|
||||
|
||||
defp refresh_bookings(socket) do
|
||||
{:ok, bookings} =
|
||||
BookingSystem.admin_search_bookings(
|
||||
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
|
||||
|
||||
{: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],
|
||||
load: [:space, :user]
|
||||
)
|
||||
|
||||
{pending, past} = Enum.split_with(bookings, &(&1.state == :requested))
|
||||
{: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],
|
||||
load: [:space, :user]
|
||||
)
|
||||
|
||||
{:noreply,
|
||||
assign(socket,
|
||||
pending_bookings: pending,
|
||||
past_bookings: past
|
||||
pending_page: pending_page,
|
||||
history_page: history_page
|
||||
)}
|
||||
end
|
||||
|
||||
defp apply_date_filter(socket, date) do
|
||||
space_id = socket.assigns.filter_space_id
|
||||
email = if socket.assigns.filter_email == "", do: nil, else: socket.assigns.filter_email
|
||||
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
|
||||
|
||||
{:ok, bookings} =
|
||||
BookingSystem.admin_search_bookings(space_id, email, date, load: [:space, :user])
|
||||
email = if filter_email == "", do: nil, else: filter_email
|
||||
|
||||
{pending, past} = Enum.split_with(bookings, &(&1.state == :requested))
|
||||
{: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_bookings: pending,
|
||||
past_bookings: past,
|
||||
filter_date: date
|
||||
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
|
||||
)
|
||||
end
|
||||
|
||||
defp status_badge_classes(:requested) do
|
||||
"bg-amber-100 dark:bg-amber-900/40 text-amber-800 dark:text-amber-200"
|
||||
defp build_path(socket, pending_page, history_page) do
|
||||
query_params = []
|
||||
|
||||
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 status_badge_classes(:accepted) do
|
||||
"bg-green-100 dark:bg-green-900/40 text-green-800 dark:text-green-200"
|
||||
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 status_badge_classes(:rejected) do
|
||||
"bg-red-100 dark:bg-red-900/40 text-red-800 dark:text-red-200"
|
||||
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
|
||||
_ -> 1
|
||||
end
|
||||
end
|
||||
|
||||
defp status_badge_classes(:cancelled) do
|
||||
"bg-slate-100 dark:bg-slate-900/40 text-slate-800 dark:text-slate-200"
|
||||
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
|
||||
end
|
||||
|
||||
defp status_badge_classes(_), do: "bg-slate-100 text-slate-800"
|
||||
|
||||
defp status_icon(:requested), do: "hero-clock"
|
||||
defp status_icon(:accepted), do: "hero-check-circle"
|
||||
defp status_icon(:rejected), do: "hero-x-circle"
|
||||
defp status_icon(:cancelled), do: "hero-minus-circle"
|
||||
defp status_icon(_), do: "hero-question-mark-circle"
|
||||
|
||||
defp status_label(:requested), do: "Pending"
|
||||
defp status_label(:accepted), do: "Confirmed"
|
||||
defp status_label(:rejected), do: "Rejected"
|
||||
defp status_label(:cancelled), do: "Cancelled"
|
||||
defp status_label(_), do: "Unknown"
|
||||
end
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@
|
|||
<span class="text-xs font-bold uppercase tracking-wider text-slate-600 dark:text-slate-400">
|
||||
Pending
|
||||
</span>
|
||||
<span class="text-2xl font-bold text-primary">{length(@pending_bookings)}</span>
|
||||
<span class="text-2xl font-bold text-primary">{@pending_page.count}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -30,6 +30,7 @@
|
|||
<div class="bg-white dark:bg-slate-800 p-5 rounded-2xl shadow-sm border border-slate-200 dark:border-slate-700">
|
||||
<form
|
||||
phx-change="filter_bookings"
|
||||
phx-submit="filter_bookings"
|
||||
class="grid grid-cols-1 md:grid-cols-12 gap-4 items-end"
|
||||
>
|
||||
<div class="col-span-1 md:col-span-4 flex flex-col gap-1.5">
|
||||
|
|
@ -105,325 +106,40 @@
|
|||
</form>
|
||||
</div>
|
||||
|
||||
<%!-- Pending Bookings Table --%>
|
||||
<%= if @pending_bookings != [] do %>
|
||||
<div>
|
||||
<h2 class="text-xl font-bold text-slate-900 dark:text-white mb-4">Pending Requests</h2>
|
||||
<div class="bg-white dark:bg-slate-800 rounded-2xl border border-slate-200 dark:border-slate-700 shadow-sm overflow-hidden">
|
||||
<div class="overflow-x-auto">
|
||||
<table class="min-w-full divide-y divide-slate-200 dark:divide-slate-700">
|
||||
<thead class="bg-slate-50 dark:bg-slate-900">
|
||||
<tr>
|
||||
<th class="px-6 py-3 text-left text-xs font-bold text-slate-600 dark:text-slate-400 uppercase tracking-wider w-[4%]">
|
||||
</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-bold text-slate-600 dark:text-slate-400 uppercase tracking-wider">
|
||||
Space
|
||||
</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-bold text-slate-600 dark:text-slate-400 uppercase tracking-wider">
|
||||
Start
|
||||
</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-bold text-slate-600 dark:text-slate-400 uppercase tracking-wider">
|
||||
End
|
||||
</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-bold text-slate-600 dark:text-slate-400 uppercase tracking-wider">
|
||||
Customer
|
||||
</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-bold text-slate-600 dark:text-slate-400 uppercase tracking-wider">
|
||||
Status
|
||||
</th>
|
||||
<th class="px-6 py-3 text-center text-xs font-bold text-slate-600 dark:text-slate-400 uppercase tracking-wider min-w-[240px]">
|
||||
Actions
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-slate-200 dark:divide-slate-700">
|
||||
<%= for booking <- @pending_bookings do %>
|
||||
<% is_expanded = MapSet.member?(@expanded_booking_ids, booking.id) %>
|
||||
<tr class={["group", if(is_expanded, do: "expanded", else: "")]}>
|
||||
<td class="px-3 py-4 whitespace-nowrap align-top">
|
||||
<button
|
||||
phx-click="toggle_expand"
|
||||
phx-value-booking_id={booking.id}
|
||||
class="flex items-center justify-center size-7 rounded-full text-slate-500 hover:bg-slate-100 dark:hover:bg-slate-700 transition-colors"
|
||||
>
|
||||
<.icon
|
||||
name="hero-chevron-down"
|
||||
class={[
|
||||
"w-4 h-4 transition-transform",
|
||||
if(is_expanded, do: "rotate-180", else: "")
|
||||
]}
|
||||
/>
|
||||
</button>
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="size-8 rounded-full bg-indigo-50 dark:bg-indigo-900/30 text-indigo-600 dark:text-indigo-400 flex items-center justify-center">
|
||||
<.icon name="hero-building-office" class="w-4 h-4" />
|
||||
</div>
|
||||
<div>
|
||||
<p class="font-medium text-slate-900 dark:text-white">
|
||||
{booking.space.name}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap">
|
||||
<p class="text-sm text-slate-900 dark:text-slate-200">
|
||||
{SpazioSolazzo.CalendarExt.format_datetime_range_start(
|
||||
booking.start_datetime
|
||||
)}
|
||||
</p>
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap">
|
||||
<p class="text-sm text-slate-900 dark:text-slate-200">
|
||||
{SpazioSolazzo.CalendarExt.format_datetime_range_end(
|
||||
booking.start_datetime,
|
||||
booking.end_datetime
|
||||
)}
|
||||
</p>
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap">
|
||||
<div>
|
||||
<p class="font-medium text-slate-900 dark:text-white">
|
||||
{booking.customer_name}
|
||||
</p>
|
||||
<p class="text-xs text-slate-600 dark:text-slate-400">
|
||||
{booking.customer_email}
|
||||
</p>
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap">
|
||||
<span class={[
|
||||
status_badge_classes(booking.state),
|
||||
"text-xs font-bold px-3 py-1 rounded-full flex items-center gap-1 w-fit"
|
||||
]}>
|
||||
<.icon name={status_icon(booking.state)} class="w-3.5 h-3.5" />
|
||||
{status_label(booking.state)}
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-center">
|
||||
<div class="flex justify-center gap-3">
|
||||
<button
|
||||
phx-click="show_reject_modal"
|
||||
phx-value-booking_id={booking.id}
|
||||
class="flex items-center justify-center px-4 py-2 rounded-lg border border-red-200 dark:border-red-900 text-red-600 dark:text-red-400 bg-white dark:bg-transparent hover:bg-red-50 dark:hover:bg-red-900/20 font-bold text-sm transition-colors"
|
||||
>
|
||||
Reject
|
||||
</button>
|
||||
<button
|
||||
phx-click="approve_booking"
|
||||
phx-value-booking_id={booking.id}
|
||||
class="flex items-center justify-center px-4 py-2 rounded-lg bg-primary hover:bg-primary-hover text-white font-bold text-sm transition-colors shadow-sm"
|
||||
>
|
||||
Confirm
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<%= if is_expanded do %>
|
||||
<tr class="bg-slate-50 dark:bg-slate-900/50">
|
||||
<td class="px-3 py-2"></td>
|
||||
<td
|
||||
class="px-6 py-4 text-sm text-slate-600 dark:text-slate-400"
|
||||
colspan="6"
|
||||
>
|
||||
<div class="flex flex-col gap-2">
|
||||
<p>
|
||||
<strong class="font-semibold text-slate-900 dark:text-white">
|
||||
Phone:
|
||||
</strong>
|
||||
<%= if booking.customer_phone do %>
|
||||
{booking.customer_phone}
|
||||
<% else %>
|
||||
<span class="italic text-slate-400">Not provided</span>
|
||||
<% end %>
|
||||
</p>
|
||||
<p>
|
||||
<strong class="font-semibold text-slate-900 dark:text-white">
|
||||
Note:
|
||||
</strong>
|
||||
<%= if booking.customer_comment do %>
|
||||
{booking.customer_comment}
|
||||
<% else %>
|
||||
<span class="italic text-slate-400">Not provided</span>
|
||||
<% end %>
|
||||
</p>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<% end %>
|
||||
<% end %>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<%!-- Past Bookings Table --%>
|
||||
<%= if @past_bookings != [] do %>
|
||||
<div>
|
||||
<h2 class="text-xl font-bold text-slate-900 dark:text-white mb-4">Booking History</h2>
|
||||
<div class="bg-white dark:bg-slate-800 rounded-2xl border border-slate-200 dark:border-slate-700 shadow-sm overflow-hidden">
|
||||
<div class="overflow-x-auto">
|
||||
<table class="min-w-full divide-y divide-slate-200 dark:divide-slate-700">
|
||||
<thead class="bg-slate-50 dark:bg-slate-900">
|
||||
<tr>
|
||||
<th class="px-6 py-3 text-left text-xs font-bold text-slate-600 dark:text-slate-400 uppercase tracking-wider w-[4%]">
|
||||
</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-bold text-slate-600 dark:text-slate-400 uppercase tracking-wider">
|
||||
Space
|
||||
</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-bold text-slate-600 dark:text-slate-400 uppercase tracking-wider">
|
||||
Start
|
||||
</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-bold text-slate-600 dark:text-slate-400 uppercase tracking-wider">
|
||||
End
|
||||
</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-bold text-slate-600 dark:text-slate-400 uppercase tracking-wider">
|
||||
Customer
|
||||
</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-bold text-slate-600 dark:text-slate-400 uppercase tracking-wider">
|
||||
Status
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-slate-200 dark:divide-slate-700">
|
||||
<%= for booking <- @past_bookings do %>
|
||||
<% is_expanded = MapSet.member?(@expanded_booking_ids, booking.id) %>
|
||||
<tr class={["group", if(is_expanded, do: "expanded", else: "")]}>
|
||||
<td class="px-3 py-4 whitespace-nowrap align-top">
|
||||
<button
|
||||
phx-click="toggle_expand"
|
||||
phx-value-booking_id={booking.id}
|
||||
class="flex items-center justify-center size-7 rounded-full text-slate-500 hover:bg-slate-100 dark:hover:bg-slate-700 transition-colors"
|
||||
>
|
||||
<.icon
|
||||
name="hero-chevron-down"
|
||||
class={[
|
||||
"w-4 h-4 transition-transform",
|
||||
if(is_expanded, do: "rotate-180", else: "")
|
||||
]}
|
||||
/>
|
||||
</button>
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="size-8 rounded-full bg-indigo-50 dark:bg-indigo-900/30 text-indigo-600 dark:text-indigo-400 flex items-center justify-center">
|
||||
<.icon name="hero-building-office" class="w-4 h-4" />
|
||||
</div>
|
||||
<div>
|
||||
<p class="font-medium text-slate-900 dark:text-white">
|
||||
{booking.space.name}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap">
|
||||
<p class="text-sm text-slate-900 dark:text-slate-200">
|
||||
{SpazioSolazzo.CalendarExt.format_datetime_range_start(
|
||||
booking.start_datetime
|
||||
)}
|
||||
</p>
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap">
|
||||
<p class="text-sm text-slate-900 dark:text-slate-200">
|
||||
{SpazioSolazzo.CalendarExt.format_datetime_range_end(
|
||||
booking.start_datetime,
|
||||
booking.end_datetime
|
||||
)}
|
||||
</p>
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap">
|
||||
<div>
|
||||
<p class="font-medium text-slate-900 dark:text-white">
|
||||
{booking.customer_name}
|
||||
</p>
|
||||
<p class="text-xs text-slate-600 dark:text-slate-400">
|
||||
{booking.customer_email}
|
||||
</p>
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap">
|
||||
<span class={[
|
||||
status_badge_classes(booking.state),
|
||||
"text-xs font-bold px-3 py-1 rounded-full flex items-center gap-1 w-fit"
|
||||
]}>
|
||||
<.icon name={status_icon(booking.state)} class="w-3.5 h-3.5" />
|
||||
{status_label(booking.state)}
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
<%= if is_expanded do %>
|
||||
<tr class="bg-slate-50 dark:bg-slate-900/50">
|
||||
<td class="px-3 py-2"></td>
|
||||
<td
|
||||
class="px-6 py-4 text-sm text-slate-600 dark:text-slate-400"
|
||||
colspan="5"
|
||||
>
|
||||
<div class="flex flex-col gap-2">
|
||||
<p>
|
||||
<strong class="font-semibold text-slate-900 dark:text-white">
|
||||
Phone:
|
||||
</strong>
|
||||
<%= if booking.customer_phone do %>
|
||||
{booking.customer_phone}
|
||||
<% else %>
|
||||
<span class="italic text-slate-400">Not provided</span>
|
||||
<% end %>
|
||||
</p>
|
||||
<p>
|
||||
<strong class="font-semibold text-slate-900 dark:text-white">
|
||||
Note:
|
||||
</strong>
|
||||
<%= if booking.customer_comment do %>
|
||||
{booking.customer_comment}
|
||||
<% else %>
|
||||
<span class="italic text-slate-400">Not provided</span>
|
||||
<% end %>
|
||||
</p>
|
||||
<%= if booking.state == :rejected do %>
|
||||
<p>
|
||||
<strong class="font-semibold text-slate-900 dark:text-white">
|
||||
Rejection Reason:
|
||||
</strong>
|
||||
<%= if booking.rejection_reason do %>
|
||||
{booking.rejection_reason}
|
||||
<% else %>
|
||||
<span class="italic text-slate-400">Not provided</span>
|
||||
<% end %>
|
||||
</p>
|
||||
<% end %>
|
||||
<%= if booking.state == :cancelled do %>
|
||||
<p>
|
||||
<strong class="font-semibold text-slate-900 dark:text-white">
|
||||
Cancellation Reason:
|
||||
</strong>
|
||||
<%= if booking.cancellation_reason do %>
|
||||
{booking.cancellation_reason}
|
||||
<% else %>
|
||||
<span class="italic text-slate-400">Not provided</span>
|
||||
<% end %>
|
||||
</p>
|
||||
<% end %>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<% end %>
|
||||
<% end %>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<%= if @pending_bookings == [] && @past_bookings == [] do %>
|
||||
<%= if @pending_page.count == 0 && @history_page.count == 0 do %>
|
||||
<div class="text-center py-12 bg-slate-50 dark:bg-slate-800/50 rounded-xl">
|
||||
<.icon name="hero-inbox" class="w-16 h-16 text-slate-400 mx-auto mb-4" />
|
||||
<p class="text-slate-500 dark:text-slate-400 text-lg">No bookings found</p>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<%!-- Pending Bookings Table --%>
|
||||
<%= if @pending_page.count > 0 do %>
|
||||
<.bookings_table
|
||||
title="Pending Requests"
|
||||
bookings={@pending_page.results}
|
||||
page={@pending_page}
|
||||
current_page={@pending_page_number}
|
||||
event_prefix="pending"
|
||||
expanded_booking_ids={@expanded_booking_ids}
|
||||
show_actions={true}
|
||||
show_cancellation_details={false}
|
||||
/>
|
||||
<% end %>
|
||||
|
||||
<%!-- Past Bookings Table --%>
|
||||
<%= if @history_page.count > 0 do %>
|
||||
<.bookings_table
|
||||
title="Booking History"
|
||||
bookings={@history_page.results}
|
||||
page={@history_page}
|
||||
current_page={@history_page_number}
|
||||
event_prefix="history"
|
||||
expanded_booking_ids={@expanded_booking_ids}
|
||||
show_actions={false}
|
||||
show_cancellation_details={true}
|
||||
/>
|
||||
<% end %>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
|
|
|
|||
1
mix.lock
1
mix.lock
|
|
@ -1,6 +1,5 @@
|
|||
%{
|
||||
"ash": {:hex, :ash, "3.12.0", "5b78000df650d86b446d88977ef8aa5c9d9f7ffa1193fa3c4b901c60bff2d130", [:mix], [{:crux, ">= 0.1.2 and < 1.0.0-0", [hex: :crux, repo: "hexpm", optional: false]}, {:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:ecto, "~> 3.7", [hex: :ecto, repo: "hexpm", optional: false]}, {:ets, "~> 0.8", [hex: :ets, repo: "hexpm", optional: false]}, {:igniter, ">= 0.6.29 and < 1.0.0-0", [hex: :igniter, repo: "hexpm", optional: true]}, {:jason, ">= 1.0.0", [hex: :jason, repo: "hexpm", optional: false]}, {:picosat_elixir, "~> 0.2", [hex: :picosat_elixir, repo: "hexpm", optional: true]}, {:plug, ">= 0.0.0", [hex: :plug, repo: "hexpm", optional: true]}, {:reactor, "~> 0.11", [hex: :reactor, repo: "hexpm", optional: false]}, {:simple_sat, ">= 0.1.1 and < 1.0.0-0", [hex: :simple_sat, repo: "hexpm", optional: true]}, {:spark, ">= 2.3.14 and < 3.0.0-0", [hex: :spark, repo: "hexpm", optional: false]}, {:splode, ">= 0.2.6 and < 1.0.0-0", [hex: :splode, repo: "hexpm", optional: false]}, {:stream_data, "~> 1.0", [hex: :stream_data, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.1", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "7cf45b4eb83aa0ab5e6707d6e4ea4a10c29ab20613c87f06344f7953b2ca5e18"},
|
||||
"ash_admin": {:hex, :ash_admin, "0.13.24", "4fafddc7b4450a92878b58630688c55cab20b0c27e35cad68f29811f73815816", [:mix], [{:ash, ">= 3.4.63 and < 4.0.0-0", [hex: :ash, repo: "hexpm", optional: false]}, {:ash_phoenix, ">= 2.1.8 and < 3.0.0-0", [hex: :ash_phoenix, repo: "hexpm", optional: false]}, {:gettext, "~> 0.26 or ~> 1.0", [hex: :gettext, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.7", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 4.1", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:phoenix_live_view, "~> 1.1-rc", [hex: :phoenix_live_view, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: false]}], "hexpm", "8f298cf6cb6635744ed75dd401295ed52513ea4df169f0f89d6a9a75dc4de4dc"},
|
||||
"ash_authentication": {:hex, :ash_authentication, "4.13.6", "95b17f0bfc00bd6e229145b90c7026f784ae81639e832de4b5c96a738de5ed46", [:mix], [{:argon2_elixir, "~> 4.0", [hex: :argon2_elixir, repo: "hexpm", optional: true]}, {:ash, "~> 3.7", [hex: :ash, repo: "hexpm", optional: false]}, {:ash_postgres, ">= 2.6.8 and < 3.0.0-0", [hex: :ash_postgres, repo: "hexpm", optional: true]}, {:assent, "> 0.2.0 and < 0.3.0", [hex: :assent, repo: "hexpm", optional: false]}, {:bcrypt_elixir, "~> 3.0", [hex: :bcrypt_elixir, repo: "hexpm", optional: false]}, {:castore, "~> 1.0", [hex: :castore, repo: "hexpm", optional: false]}, {:finch, "~> 0.19", [hex: :finch, repo: "hexpm", optional: false]}, {:igniter, "~> 0.4", [hex: :igniter, repo: "hexpm", optional: true]}, {:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}, {:joken, "~> 2.5", [hex: :joken, repo: "hexpm", optional: false]}, {:plug, "~> 1.13", [hex: :plug, repo: "hexpm", optional: false]}, {:spark, "~> 2.0", [hex: :spark, repo: "hexpm", optional: false]}, {:splode, "~> 0.2", [hex: :splode, repo: "hexpm", optional: false]}], "hexpm", "27ded84bdc61fd267794dee17a6cbe6e52d0f365d3e8ea0460d95977b82ac6f1"},
|
||||
"ash_authentication_phoenix": {:hex, :ash_authentication_phoenix, "2.14.1", "60d127a73c2144b39fa3dab045cc3f7fce0c3ccd2dd3e8534288f5da65f0c1db", [:mix], [{:ash, "~> 3.0", [hex: :ash, repo: "hexpm", optional: false]}, {:ash_authentication, "~> 4.10", [hex: :ash_authentication, repo: "hexpm", optional: false]}, {:ash_phoenix, ">= 2.3.11 and < 3.0.0-0", [hex: :ash_phoenix, repo: "hexpm", optional: false]}, {:bcrypt_elixir, "~> 3.0", [hex: :bcrypt_elixir, repo: "hexpm", optional: false]}, {:gettext, "~> 0.26 or ~> 1.0", [hex: :gettext, repo: "hexpm", optional: true]}, {:igniter, ">= 0.5.25 and < 1.0.0-0", [hex: :igniter, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.6", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:phoenix_html_helpers, "~> 1.0", [hex: :phoenix_html_helpers, repo: "hexpm", optional: false]}, {:phoenix_live_view, "~> 1.1", [hex: :phoenix_live_view, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: false]}, {:slugify, "~> 1.3", [hex: :slugify, repo: "hexpm", optional: false]}], "hexpm", "3cd57aee855be3ccf2960ce0b005ad209c97fbfc81faa71212bcfbd6a4a90cae"},
|
||||
"ash_phoenix": {:hex, :ash_phoenix, "2.3.18", "fad1b8af1405758888086de568f08650c2911ee97074cfe2e325b14854bc43dd", [:mix], [{:ash, ">= 3.5.13 and < 4.0.0-0", [hex: :ash, repo: "hexpm", optional: false]}, {:igniter, "~> 0.6", [hex: :igniter, repo: "hexpm", optional: true]}, {:inertia, "~> 2.3", [hex: :inertia, repo: "hexpm", optional: true]}, {:phoenix, "~> 1.5.6 or ~> 1.6", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:phoenix_live_view, "~> 0.20.3 or ~> 1.0-rc.1", [hex: :phoenix_live_view, repo: "hexpm", optional: false]}, {:spark, ">= 2.2.29 and < 3.0.0-0", [hex: :spark, repo: "hexpm", optional: false]}], "hexpm", "7ec28f9216221e83b90d9c3605e9d1cdd228984e09a1a86c9b9d393cebf25222"},
|
||||
|
|
|
|||
399
test/spazio_solazzo/booking_system/booking_pagination_test.exs
Normal file
399
test/spazio_solazzo/booking_system/booking_pagination_test.exs
Normal file
|
|
@ -0,0 +1,399 @@
|
|||
defmodule SpazioSolazzo.BookingSystem.BookingPaginationTest do
|
||||
use ExUnit.Case, async: true
|
||||
use SpazioSolazzo.DataCase
|
||||
|
||||
alias SpazioSolazzo.BookingSystem
|
||||
|
||||
describe "read_pending_bookings/3 pagination" do
|
||||
setup do
|
||||
{:ok, space} =
|
||||
BookingSystem.create_space(
|
||||
"Coworking",
|
||||
"coworking-pagination-test",
|
||||
"Test space for pagination",
|
||||
10
|
||||
)
|
||||
|
||||
base_date = Date.add(Date.utc_today(), 1)
|
||||
|
||||
pending_bookings =
|
||||
for i <- 1..15 do
|
||||
{:ok, booking} =
|
||||
BookingSystem.create_booking(
|
||||
space.id,
|
||||
nil,
|
||||
base_date,
|
||||
~T[09:00:00],
|
||||
~T[10:00:00],
|
||||
"Customer #{i}",
|
||||
"customer#{i}@example.com",
|
||||
nil,
|
||||
nil
|
||||
)
|
||||
|
||||
booking
|
||||
end
|
||||
|
||||
%{space: space, pending_bookings: pending_bookings, tomorrow: base_date}
|
||||
end
|
||||
|
||||
test "returns first page with default limit of 10" do
|
||||
{:ok, page} =
|
||||
BookingSystem.read_pending_bookings(
|
||||
nil,
|
||||
nil,
|
||||
nil,
|
||||
page: [limit: 10, offset: 0, count: true]
|
||||
)
|
||||
|
||||
assert length(page.results) == 10
|
||||
assert page.count == 15
|
||||
assert page.limit == 10
|
||||
assert page.offset == 0
|
||||
assert page.more? == true
|
||||
end
|
||||
|
||||
test "returns second page correctly" do
|
||||
{:ok, page} =
|
||||
BookingSystem.read_pending_bookings(
|
||||
nil,
|
||||
nil,
|
||||
nil,
|
||||
page: [limit: 10, offset: 10, count: true]
|
||||
)
|
||||
|
||||
assert length(page.results) == 5
|
||||
assert page.count == 15
|
||||
assert page.more? == false
|
||||
end
|
||||
|
||||
test "filters by space_id", %{space: space} do
|
||||
{:ok, other_space} =
|
||||
BookingSystem.create_space(
|
||||
"Other Space",
|
||||
"other-space-pagination",
|
||||
"Another test space",
|
||||
5
|
||||
)
|
||||
|
||||
tomorrow = Date.add(Date.utc_today(), 1)
|
||||
|
||||
{:ok, _} =
|
||||
BookingSystem.create_booking(
|
||||
other_space.id,
|
||||
nil,
|
||||
tomorrow,
|
||||
~T[10:00:00],
|
||||
~T[11:00:00],
|
||||
"Other Customer",
|
||||
"other@example.com",
|
||||
nil,
|
||||
nil
|
||||
)
|
||||
|
||||
{:ok, page} =
|
||||
BookingSystem.read_pending_bookings(
|
||||
space.id,
|
||||
nil,
|
||||
nil,
|
||||
page: [limit: 10, offset: 0, count: true]
|
||||
)
|
||||
|
||||
assert page.count == 15
|
||||
assert Enum.all?(page.results, fn b -> b.space_id == space.id end)
|
||||
end
|
||||
|
||||
test "filters by email" do
|
||||
{:ok, page} =
|
||||
BookingSystem.read_pending_bookings(
|
||||
nil,
|
||||
"customer1@example.com",
|
||||
nil,
|
||||
page: [limit: 10, offset: 0, count: true]
|
||||
)
|
||||
|
||||
assert page.count == 1
|
||||
assert hd(page.results).customer_email == "customer1@example.com"
|
||||
end
|
||||
|
||||
test "filters by date", %{tomorrow: tomorrow} do
|
||||
{:ok, page} =
|
||||
BookingSystem.read_pending_bookings(
|
||||
nil,
|
||||
nil,
|
||||
tomorrow,
|
||||
page: [limit: 20, offset: 0, count: true]
|
||||
)
|
||||
|
||||
assert page.count == 15
|
||||
assert Enum.all?(page.results, fn b -> DateTime.to_date(b.start_datetime) == tomorrow end)
|
||||
end
|
||||
|
||||
test "sorts by inserted_at descending (newest first)" do
|
||||
{:ok, page} =
|
||||
BookingSystem.read_pending_bookings(
|
||||
nil,
|
||||
nil,
|
||||
nil,
|
||||
page: [limit: 2, offset: 0, count: true]
|
||||
)
|
||||
|
||||
[first, second] = page.results
|
||||
assert DateTime.compare(first.inserted_at, second.inserted_at) in [:gt, :eq]
|
||||
end
|
||||
|
||||
test "returns empty results when no bookings match" do
|
||||
{:ok, page} =
|
||||
BookingSystem.read_pending_bookings(
|
||||
nil,
|
||||
"nonexistent@example.com",
|
||||
nil,
|
||||
page: [limit: 10, offset: 0, count: true]
|
||||
)
|
||||
|
||||
assert page.results == []
|
||||
assert page.count == 0
|
||||
assert page.more? == false
|
||||
end
|
||||
|
||||
test "only returns requested state bookings", %{pending_bookings: bookings} do
|
||||
[first_booking | _] = bookings
|
||||
|
||||
{:ok, _} = BookingSystem.approve_booking(first_booking)
|
||||
|
||||
{:ok, page} =
|
||||
BookingSystem.read_pending_bookings(
|
||||
nil,
|
||||
nil,
|
||||
nil,
|
||||
page: [limit: 20, offset: 0, count: true],
|
||||
load: [:space]
|
||||
)
|
||||
|
||||
assert page.count == 14
|
||||
assert Enum.all?(page.results, fn b -> b.state == :requested end)
|
||||
end
|
||||
|
||||
test "combined filters work together", %{space: space, tomorrow: tomorrow} do
|
||||
{:ok, page} =
|
||||
BookingSystem.read_pending_bookings(
|
||||
space.id,
|
||||
"customer5@example.com",
|
||||
tomorrow,
|
||||
page: [limit: 10, offset: 0, count: true]
|
||||
)
|
||||
|
||||
assert page.count == 1
|
||||
result = hd(page.results)
|
||||
assert result.space_id == space.id
|
||||
assert result.customer_email == "customer5@example.com"
|
||||
assert DateTime.to_date(result.start_datetime) == tomorrow
|
||||
end
|
||||
end
|
||||
|
||||
describe "read_booking_history/3 pagination" do
|
||||
setup do
|
||||
{:ok, space} =
|
||||
BookingSystem.create_space(
|
||||
"Coworking History",
|
||||
"coworking-history-test",
|
||||
"Test space for history pagination",
|
||||
10
|
||||
)
|
||||
|
||||
base_date = Date.add(Date.utc_today(), 1)
|
||||
|
||||
for i <- 1..30 do
|
||||
{:ok, booking} =
|
||||
BookingSystem.create_booking(
|
||||
space.id,
|
||||
nil,
|
||||
base_date,
|
||||
~T[09:00:00],
|
||||
~T[10:00:00],
|
||||
"Customer #{i}",
|
||||
"customer#{i}@example.com",
|
||||
nil,
|
||||
nil
|
||||
)
|
||||
|
||||
cond do
|
||||
rem(i, 10) == 0 ->
|
||||
BookingSystem.reject_booking(booking, "Test rejection")
|
||||
|
||||
rem(i, 5) == 0 ->
|
||||
{:ok, approved} = BookingSystem.approve_booking(booking)
|
||||
BookingSystem.cancel_booking(approved, "Test cancellation")
|
||||
|
||||
true ->
|
||||
BookingSystem.approve_booking(booking)
|
||||
end
|
||||
end
|
||||
|
||||
%{space: space, tomorrow: base_date}
|
||||
end
|
||||
|
||||
test "returns first page with default limit of 25" do
|
||||
{:ok, page} =
|
||||
BookingSystem.read_booking_history(
|
||||
nil,
|
||||
nil,
|
||||
nil,
|
||||
page: [limit: 25, offset: 0, count: true]
|
||||
)
|
||||
|
||||
assert length(page.results) == 25
|
||||
assert page.count == 30
|
||||
assert page.more? == true
|
||||
end
|
||||
|
||||
test "returns second page correctly" do
|
||||
{:ok, page} =
|
||||
BookingSystem.read_booking_history(
|
||||
nil,
|
||||
nil,
|
||||
nil,
|
||||
page: [limit: 25, offset: 25, count: true]
|
||||
)
|
||||
|
||||
assert length(page.results) == 5
|
||||
assert page.count == 30
|
||||
assert page.more? == false
|
||||
end
|
||||
|
||||
test "returns only accepted, rejected, and cancelled bookings", %{space: space} do
|
||||
tomorrow = Date.add(Date.utc_today(), 2)
|
||||
|
||||
{:ok, _pending} =
|
||||
BookingSystem.create_booking(
|
||||
space.id,
|
||||
nil,
|
||||
tomorrow,
|
||||
~T[10:00:00],
|
||||
~T[11:00:00],
|
||||
"Pending Customer",
|
||||
"pending@example.com",
|
||||
nil,
|
||||
nil
|
||||
)
|
||||
|
||||
{:ok, page} =
|
||||
BookingSystem.read_booking_history(
|
||||
nil,
|
||||
nil,
|
||||
nil,
|
||||
page: [limit: 50, offset: 0, count: true]
|
||||
)
|
||||
|
||||
assert page.count == 30
|
||||
assert Enum.all?(page.results, fn b -> b.state in [:accepted, :rejected, :cancelled] end)
|
||||
end
|
||||
|
||||
test "sorts by start_datetime descending (most recent first)" do
|
||||
{:ok, page} =
|
||||
BookingSystem.read_booking_history(
|
||||
nil,
|
||||
nil,
|
||||
nil,
|
||||
page: [limit: 2, offset: 0, count: true]
|
||||
)
|
||||
|
||||
[first, second] = page.results
|
||||
assert DateTime.compare(first.start_datetime, second.start_datetime) in [:gt, :eq]
|
||||
end
|
||||
|
||||
test "filters by space_id", %{space: space} do
|
||||
{:ok, other_space} =
|
||||
BookingSystem.create_space(
|
||||
"Other History Space",
|
||||
"other-history-space",
|
||||
"Another test space",
|
||||
5
|
||||
)
|
||||
|
||||
tomorrow = Date.add(Date.utc_today(), 1)
|
||||
|
||||
{:ok, other_booking} =
|
||||
BookingSystem.create_booking(
|
||||
other_space.id,
|
||||
nil,
|
||||
tomorrow,
|
||||
~T[10:00:00],
|
||||
~T[11:00:00],
|
||||
"Other Customer",
|
||||
"other@example.com",
|
||||
nil,
|
||||
nil
|
||||
)
|
||||
|
||||
BookingSystem.approve_booking(other_booking)
|
||||
|
||||
{:ok, page} =
|
||||
BookingSystem.read_booking_history(
|
||||
space.id,
|
||||
nil,
|
||||
nil,
|
||||
page: [limit: 50, offset: 0, count: true]
|
||||
)
|
||||
|
||||
assert page.count == 30
|
||||
assert Enum.all?(page.results, fn b -> b.space_id == space.id end)
|
||||
end
|
||||
|
||||
test "filters by email" do
|
||||
{:ok, page} =
|
||||
BookingSystem.read_booking_history(
|
||||
nil,
|
||||
"customer1@example.com",
|
||||
nil,
|
||||
page: [limit: 10, offset: 0, count: true]
|
||||
)
|
||||
|
||||
assert page.count == 1
|
||||
assert hd(page.results).customer_email == "customer1@example.com"
|
||||
end
|
||||
|
||||
test "filters by date", %{tomorrow: tomorrow} do
|
||||
{:ok, page} =
|
||||
BookingSystem.read_booking_history(
|
||||
nil,
|
||||
nil,
|
||||
tomorrow,
|
||||
page: [limit: 50, offset: 0, count: true]
|
||||
)
|
||||
|
||||
assert page.count == 30
|
||||
assert Enum.all?(page.results, fn b -> DateTime.to_date(b.start_datetime) == tomorrow end)
|
||||
end
|
||||
|
||||
test "combined filters work together", %{space: space, tomorrow: tomorrow} do
|
||||
{:ok, page} =
|
||||
BookingSystem.read_booking_history(
|
||||
space.id,
|
||||
"customer2@example.com",
|
||||
tomorrow,
|
||||
page: [limit: 10, offset: 0, count: true]
|
||||
)
|
||||
|
||||
assert page.count == 1
|
||||
result = hd(page.results)
|
||||
assert result.space_id == space.id
|
||||
assert result.customer_email == "customer2@example.com"
|
||||
assert DateTime.to_date(result.start_datetime) == tomorrow
|
||||
end
|
||||
|
||||
test "returns empty results when no bookings match" do
|
||||
{:ok, page} =
|
||||
BookingSystem.read_booking_history(
|
||||
nil,
|
||||
"nonexistent@example.com",
|
||||
nil,
|
||||
page: [limit: 25, offset: 0, count: true]
|
||||
)
|
||||
|
||||
assert page.results == []
|
||||
assert page.count == 0
|
||||
assert page.more? == false
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -493,129 +493,6 @@ defmodule SpazioSolazzo.BookingSystem.BookingTest do
|
|||
end
|
||||
end
|
||||
|
||||
describe "admin_search_bookings/3" do
|
||||
test "returns pending and approved bookings for space", %{space: space, date: date} do
|
||||
{:ok, pending} =
|
||||
request_booking(
|
||||
space.id,
|
||||
nil,
|
||||
date,
|
||||
~T[09:00:00],
|
||||
~T[10:00:00],
|
||||
"User 1",
|
||||
"user1@example.com",
|
||||
"",
|
||||
""
|
||||
)
|
||||
|
||||
{:ok, approved} =
|
||||
request_booking(
|
||||
space.id,
|
||||
nil,
|
||||
date,
|
||||
~T[10:00:00],
|
||||
~T[11:00:00],
|
||||
"User 2",
|
||||
"user2@example.com",
|
||||
"",
|
||||
""
|
||||
)
|
||||
|
||||
{:ok, _} = BookingSystem.approve_booking(approved.id)
|
||||
|
||||
{:ok, cancelled} =
|
||||
request_booking(
|
||||
space.id,
|
||||
nil,
|
||||
date,
|
||||
~T[11:00:00],
|
||||
~T[12:00:00],
|
||||
"User 3",
|
||||
"user3@example.com",
|
||||
"",
|
||||
""
|
||||
)
|
||||
|
||||
{:ok, _} = BookingSystem.cancel_booking(cancelled.id, "Test cancellation")
|
||||
|
||||
{:ok, bookings} = BookingSystem.admin_search_bookings(space.id, nil, nil)
|
||||
|
||||
assert length(bookings) == 2
|
||||
assert Enum.any?(bookings, &(&1.id == pending.id))
|
||||
assert Enum.any?(bookings, &(&1.id == approved.id))
|
||||
refute Enum.any?(bookings, &(&1.id == cancelled.id))
|
||||
end
|
||||
|
||||
test "filters by email", %{space: space, date: date} do
|
||||
{:ok, _booking1} =
|
||||
request_booking(
|
||||
space.id,
|
||||
nil,
|
||||
date,
|
||||
~T[09:00:00],
|
||||
~T[10:00:00],
|
||||
"User 1",
|
||||
"user1@example.com",
|
||||
"",
|
||||
""
|
||||
)
|
||||
|
||||
{:ok, booking2} =
|
||||
request_booking(
|
||||
space.id,
|
||||
nil,
|
||||
date,
|
||||
~T[10:00:00],
|
||||
~T[11:00:00],
|
||||
"User 2",
|
||||
"user2@example.com",
|
||||
"",
|
||||
""
|
||||
)
|
||||
|
||||
{:ok, bookings} =
|
||||
BookingSystem.admin_search_bookings(space.id, "user2@example.com", nil)
|
||||
|
||||
assert length(bookings) == 1
|
||||
assert hd(bookings).id == booking2.id
|
||||
end
|
||||
|
||||
test "filters by date", %{space: space, date: date} do
|
||||
other_date = Date.add(date, 1)
|
||||
|
||||
{:ok, booking1} =
|
||||
request_booking(
|
||||
space.id,
|
||||
nil,
|
||||
date,
|
||||
~T[09:00:00],
|
||||
~T[10:00:00],
|
||||
"User 1",
|
||||
"user1@example.com",
|
||||
"",
|
||||
""
|
||||
)
|
||||
|
||||
{:ok, _booking2} =
|
||||
request_booking(
|
||||
space.id,
|
||||
nil,
|
||||
other_date,
|
||||
~T[09:00:00],
|
||||
~T[10:00:00],
|
||||
"User 2",
|
||||
"user2@example.com",
|
||||
"",
|
||||
""
|
||||
)
|
||||
|
||||
{:ok, bookings} = BookingSystem.admin_search_bookings(space.id, nil, date)
|
||||
|
||||
assert length(bookings) == 1
|
||||
assert hd(bookings).id == booking1.id
|
||||
end
|
||||
end
|
||||
|
||||
describe "count pending requests" do
|
||||
test "returns only pending bookings", %{space: space, date: date} do
|
||||
{:ok, _pending1} =
|
||||
|
|
|
|||
|
|
@ -0,0 +1,582 @@
|
|||
defmodule SpazioSolazzoWeb.Admin.BookingManagementPaginationTest do
|
||||
use SpazioSolazzoWeb.ConnCase, async: false
|
||||
|
||||
import Phoenix.LiveViewTest
|
||||
import SpazioSolazzo.AuthHelpers
|
||||
import Ecto.Query
|
||||
|
||||
alias SpazioSolazzo.BookingSystem
|
||||
|
||||
defp create_admin_user do
|
||||
user = register_user("admin@example.com", "Admin User")
|
||||
{:ok, uuid} = Ecto.UUID.dump(user.id)
|
||||
|
||||
from(u in "users", where: u.id == ^uuid)
|
||||
|> SpazioSolazzo.Repo.update_all(set: [role: "admin"])
|
||||
|
||||
user
|
||||
end
|
||||
|
||||
setup do
|
||||
{:ok, space} =
|
||||
BookingSystem.create_space(
|
||||
"Coworking",
|
||||
"coworking-pagination",
|
||||
"Coworking space",
|
||||
10
|
||||
)
|
||||
|
||||
admin_user = create_admin_user()
|
||||
tomorrow = Date.add(Date.utc_today(), 1)
|
||||
|
||||
%{space: space, admin_user: admin_user, tomorrow: tomorrow}
|
||||
end
|
||||
|
||||
describe "pagination - pending bookings" do
|
||||
test "displays first page of pending bookings", %{
|
||||
conn: conn,
|
||||
admin_user: admin_user,
|
||||
space: space,
|
||||
tomorrow: tomorrow
|
||||
} do
|
||||
user = register_user("user#{System.unique_integer([:positive])}@example.com", "Test User")
|
||||
|
||||
for i <- 1..15 do
|
||||
hour = rem(8 + i, 24)
|
||||
start_time = Time.new!(hour, 0, 0)
|
||||
end_time = Time.new!(hour, 30, 0)
|
||||
|
||||
BookingSystem.create_booking(
|
||||
space.id,
|
||||
user.id,
|
||||
tomorrow,
|
||||
start_time,
|
||||
end_time,
|
||||
"Customer #{i}",
|
||||
"customer#{i}@example.com",
|
||||
nil,
|
||||
nil
|
||||
)
|
||||
end
|
||||
|
||||
conn = log_in_user(conn, admin_user)
|
||||
{:ok, view, html} = live(conn, "/admin/bookings")
|
||||
|
||||
assert html =~ "Showing 1-10 of 15"
|
||||
assert has_element?(view, "button[phx-click='pending_page_change']")
|
||||
end
|
||||
|
||||
test "navigates to second page of pending bookings", %{
|
||||
conn: conn,
|
||||
admin_user: admin_user,
|
||||
space: space,
|
||||
tomorrow: tomorrow
|
||||
} do
|
||||
user = register_user("user#{System.unique_integer([:positive])}@example.com", "Test User")
|
||||
|
||||
for i <- 1..15 do
|
||||
hour = rem(8 + i, 24)
|
||||
start_time = Time.new!(hour, 0, 0)
|
||||
end_time = Time.new!(hour, 30, 0)
|
||||
|
||||
BookingSystem.create_booking(
|
||||
space.id,
|
||||
user.id,
|
||||
tomorrow,
|
||||
start_time,
|
||||
end_time,
|
||||
"Customer #{i}",
|
||||
"customer#{i}@example.com",
|
||||
nil,
|
||||
nil
|
||||
)
|
||||
end
|
||||
|
||||
conn = log_in_user(conn, admin_user)
|
||||
{:ok, view, _html} = live(conn, "/admin/bookings")
|
||||
|
||||
html =
|
||||
view
|
||||
|> element("button[phx-click='pending_page_change']", "2")
|
||||
|> render_click()
|
||||
|
||||
assert html =~ "Showing 11-15 of 15"
|
||||
end
|
||||
|
||||
test "pagination URL params are updated", %{
|
||||
conn: conn,
|
||||
admin_user: admin_user,
|
||||
space: space,
|
||||
tomorrow: tomorrow
|
||||
} do
|
||||
user = register_user("user#{System.unique_integer([:positive])}@example.com", "Test User")
|
||||
|
||||
for i <- 1..15 do
|
||||
hour = rem(8 + i, 24)
|
||||
start_time = Time.new!(hour, 0, 0)
|
||||
end_time = Time.new!(hour, 30, 0)
|
||||
|
||||
BookingSystem.create_booking(
|
||||
space.id,
|
||||
user.id,
|
||||
tomorrow,
|
||||
start_time,
|
||||
end_time,
|
||||
"Customer #{i}",
|
||||
"customer#{i}@example.com",
|
||||
nil,
|
||||
nil
|
||||
)
|
||||
end
|
||||
|
||||
conn = log_in_user(conn, admin_user)
|
||||
{:ok, view, _html} = live(conn, "/admin/bookings")
|
||||
|
||||
view
|
||||
|> element("button[phx-click='pending_page_change']", "2")
|
||||
|> render_click()
|
||||
|
||||
assert_patch(view, "/admin/bookings?pending_page=2")
|
||||
end
|
||||
|
||||
test "filters reset pagination to page 1", %{
|
||||
conn: conn,
|
||||
admin_user: admin_user,
|
||||
space: space,
|
||||
tomorrow: tomorrow
|
||||
} do
|
||||
user = register_user("user#{System.unique_integer([:positive])}@example.com", "Test User")
|
||||
|
||||
for i <- 1..15 do
|
||||
hour = rem(8 + i, 24)
|
||||
start_time = Time.new!(hour, 0, 0)
|
||||
end_time = Time.new!(hour, 30, 0)
|
||||
|
||||
BookingSystem.create_booking(
|
||||
space.id,
|
||||
user.id,
|
||||
tomorrow,
|
||||
start_time,
|
||||
end_time,
|
||||
"Customer #{i}",
|
||||
"customer#{i}@example.com",
|
||||
nil,
|
||||
nil
|
||||
)
|
||||
end
|
||||
|
||||
conn = log_in_user(conn, admin_user)
|
||||
{:ok, view, _html} = live(conn, "/admin/bookings?pending_page=2")
|
||||
|
||||
view
|
||||
|> form("form", %{"email" => "customer1@example.com"})
|
||||
|> render_change()
|
||||
|
||||
path = assert_patch(view)
|
||||
refute path =~ "pending_page=2"
|
||||
end
|
||||
|
||||
test "previous button is disabled on first page", %{
|
||||
conn: conn,
|
||||
admin_user: admin_user,
|
||||
space: space,
|
||||
tomorrow: tomorrow
|
||||
} do
|
||||
user = register_user("user#{System.unique_integer([:positive])}@example.com", "Test User")
|
||||
|
||||
for i <- 1..15 do
|
||||
hour = rem(8 + i, 24)
|
||||
start_time = Time.new!(hour, 0, 0)
|
||||
end_time = Time.new!(hour, 30, 0)
|
||||
|
||||
BookingSystem.create_booking(
|
||||
space.id,
|
||||
user.id,
|
||||
tomorrow,
|
||||
start_time,
|
||||
end_time,
|
||||
"Customer #{i}",
|
||||
"customer#{i}@example.com",
|
||||
nil,
|
||||
nil
|
||||
)
|
||||
end
|
||||
|
||||
conn = log_in_user(conn, admin_user)
|
||||
{:ok, view, html} = live(conn, "/admin/bookings")
|
||||
|
||||
assert html =~ "disabled"
|
||||
assert has_element?(view, "button[phx-click='pending_page_change'][disabled]")
|
||||
end
|
||||
end
|
||||
|
||||
describe "pagination - booking history" do
|
||||
test "displays first page of booking history", %{
|
||||
conn: conn,
|
||||
admin_user: admin_user,
|
||||
space: space,
|
||||
tomorrow: tomorrow
|
||||
} do
|
||||
for i <- 1..30 do
|
||||
start_datetime = DateTime.new!(tomorrow, Time.add(~T[09:00:00], i * 3600), "Etc/UTC")
|
||||
end_datetime = DateTime.add(start_datetime, 3600, :second)
|
||||
|
||||
{:ok, booking} =
|
||||
BookingSystem.create_walk_in(
|
||||
space.id,
|
||||
start_datetime,
|
||||
end_datetime,
|
||||
"Customer #{i}",
|
||||
"customer#{i}@example.com",
|
||||
nil
|
||||
)
|
||||
|
||||
BookingSystem.approve_booking(booking)
|
||||
end
|
||||
|
||||
conn = log_in_user(conn, admin_user)
|
||||
{:ok, view, html} = live(conn, "/admin/bookings")
|
||||
|
||||
assert html =~ "Showing 1-10 of 30"
|
||||
assert has_element?(view, "button[phx-click='history_page_change'][phx-value-page='2']")
|
||||
end
|
||||
|
||||
test "navigates to second page of booking history", %{
|
||||
conn: conn,
|
||||
admin_user: admin_user,
|
||||
space: space,
|
||||
tomorrow: tomorrow
|
||||
} do
|
||||
for i <- 1..30 do
|
||||
start_datetime = DateTime.new!(tomorrow, Time.add(~T[09:00:00], i * 3600), "Etc/UTC")
|
||||
end_datetime = DateTime.add(start_datetime, 3600, :second)
|
||||
|
||||
{:ok, booking} =
|
||||
BookingSystem.create_walk_in(
|
||||
space.id,
|
||||
start_datetime,
|
||||
end_datetime,
|
||||
"Customer #{i}",
|
||||
"customer#{i}@example.com",
|
||||
nil
|
||||
)
|
||||
|
||||
BookingSystem.approve_booking(booking)
|
||||
end
|
||||
|
||||
conn = log_in_user(conn, admin_user)
|
||||
{:ok, view, _html} = live(conn, "/admin/bookings")
|
||||
|
||||
html =
|
||||
view
|
||||
|> element("button[phx-click='history_page_change']", "2")
|
||||
|> render_click()
|
||||
|
||||
assert html =~ "Showing 11-20 of 30"
|
||||
end
|
||||
|
||||
test "history pagination URL params are updated", %{
|
||||
conn: conn,
|
||||
admin_user: admin_user,
|
||||
space: space,
|
||||
tomorrow: tomorrow
|
||||
} do
|
||||
for i <- 1..30 do
|
||||
start_datetime = DateTime.new!(tomorrow, Time.add(~T[09:00:00], i * 3600), "Etc/UTC")
|
||||
end_datetime = DateTime.add(start_datetime, 3600, :second)
|
||||
|
||||
{:ok, booking} =
|
||||
BookingSystem.create_walk_in(
|
||||
space.id,
|
||||
start_datetime,
|
||||
end_datetime,
|
||||
"Customer #{i}",
|
||||
"customer#{i}@example.com",
|
||||
nil
|
||||
)
|
||||
|
||||
BookingSystem.approve_booking(booking)
|
||||
end
|
||||
|
||||
conn = log_in_user(conn, admin_user)
|
||||
{:ok, view, _html} = live(conn, "/admin/bookings")
|
||||
|
||||
view
|
||||
|> element("button[phx-click='history_page_change']", "2")
|
||||
|> render_click()
|
||||
|
||||
assert_patch(view, "/admin/bookings?history_page=2")
|
||||
end
|
||||
end
|
||||
|
||||
describe "pagination with booking management" do
|
||||
test "approving booking refreshes current page", %{
|
||||
conn: conn,
|
||||
admin_user: admin_user,
|
||||
space: space,
|
||||
tomorrow: tomorrow
|
||||
} do
|
||||
user = register_user("user#{System.unique_integer([:positive])}@example.com", "Test User")
|
||||
|
||||
{:ok, booking} =
|
||||
BookingSystem.create_booking(
|
||||
space.id,
|
||||
user.id,
|
||||
tomorrow,
|
||||
~T[10:00:00],
|
||||
~T[11:00:00],
|
||||
"Test Customer",
|
||||
"test@example.com",
|
||||
nil,
|
||||
nil
|
||||
)
|
||||
|
||||
conn = log_in_user(conn, admin_user)
|
||||
{:ok, view, _html} = live(conn, "/admin/bookings")
|
||||
|
||||
# Trigger a refresh
|
||||
view |> form("form", %{"email" => ""}) |> render_change()
|
||||
|
||||
view
|
||||
|> element("button[phx-click='approve_booking'][phx-value-booking_id='#{booking.id}']")
|
||||
|> render_click()
|
||||
|
||||
# Just verify the view still works after approval
|
||||
html = render(view)
|
||||
assert html =~ "Manage Bookings"
|
||||
end
|
||||
|
||||
test "rejecting booking refreshes current page", %{
|
||||
conn: conn,
|
||||
admin_user: admin_user,
|
||||
space: space,
|
||||
tomorrow: tomorrow
|
||||
} do
|
||||
user = register_user("user#{System.unique_integer([:positive])}@example.com", "Test User")
|
||||
|
||||
{:ok, booking} =
|
||||
BookingSystem.create_booking(
|
||||
space.id,
|
||||
user.id,
|
||||
tomorrow,
|
||||
~T[14:00:00],
|
||||
~T[15:00:00],
|
||||
"Test Customer",
|
||||
"test2@example.com",
|
||||
nil,
|
||||
nil
|
||||
)
|
||||
|
||||
conn = log_in_user(conn, admin_user)
|
||||
{:ok, view, _html} = live(conn, "/admin/bookings")
|
||||
|
||||
# Trigger a refresh
|
||||
view |> form("form", %{"email" => ""}) |> render_change()
|
||||
|
||||
view
|
||||
|> element("button[phx-click='show_reject_modal'][phx-value-booking_id='#{booking.id}']")
|
||||
|> render_click()
|
||||
|
||||
view
|
||||
|> element("textarea[name='reason']")
|
||||
|> render_change(%{"reason" => "Test rejection"})
|
||||
|
||||
view
|
||||
|> element("form[phx-submit='confirm_reject']")
|
||||
|> render_submit()
|
||||
|
||||
# Just verify the view still works after rejection
|
||||
html = render(view)
|
||||
assert html =~ "Manage Bookings"
|
||||
end
|
||||
end
|
||||
|
||||
describe "empty states with pagination" do
|
||||
test "shows empty state when no bookings exist", %{
|
||||
conn: conn,
|
||||
admin_user: admin_user
|
||||
} do
|
||||
conn = log_in_user(conn, admin_user)
|
||||
{:ok, _view, html} = live(conn, "/admin/bookings")
|
||||
|
||||
assert html =~ "No bookings found"
|
||||
end
|
||||
|
||||
test "shows pending count as 0 when no pending bookings", %{
|
||||
conn: conn,
|
||||
admin_user: admin_user,
|
||||
space: space,
|
||||
tomorrow: tomorrow
|
||||
} do
|
||||
start_datetime = DateTime.new!(tomorrow, ~T[10:00:00], "Etc/UTC")
|
||||
end_datetime = DateTime.add(start_datetime, 3600, :second)
|
||||
|
||||
{:ok, booking} =
|
||||
BookingSystem.create_walk_in(
|
||||
space.id,
|
||||
start_datetime,
|
||||
end_datetime,
|
||||
"Customer",
|
||||
"customer@example.com",
|
||||
nil
|
||||
)
|
||||
|
||||
BookingSystem.approve_booking(booking)
|
||||
|
||||
conn = log_in_user(conn, admin_user)
|
||||
{:ok, _view, html} = live(conn, "/admin/bookings")
|
||||
|
||||
assert html =~ "<span class=\"text-2xl font-bold text-primary\">0</span>"
|
||||
end
|
||||
end
|
||||
|
||||
describe "pagination with filters" do
|
||||
test "pagination works with space filter", %{
|
||||
conn: conn,
|
||||
admin_user: admin_user,
|
||||
space: space,
|
||||
tomorrow: tomorrow
|
||||
} do
|
||||
user = register_user("user#{System.unique_integer([:positive])}@example.com", "Test User")
|
||||
|
||||
for i <- 1..15 do
|
||||
hour = rem(8 + i, 24)
|
||||
start_time = Time.new!(hour, 0, 0)
|
||||
end_time = Time.new!(hour, 30, 0)
|
||||
|
||||
BookingSystem.create_booking(
|
||||
space.id,
|
||||
user.id,
|
||||
tomorrow,
|
||||
start_time,
|
||||
end_time,
|
||||
"Customer #{i}",
|
||||
"customer#{i}@example.com",
|
||||
nil,
|
||||
nil
|
||||
)
|
||||
end
|
||||
|
||||
conn = log_in_user(conn, admin_user)
|
||||
{:ok, view, _html} = live(conn, "/admin/bookings")
|
||||
|
||||
view
|
||||
|> form("form", %{"space_id" => space.id})
|
||||
|> render_change()
|
||||
|
||||
html = render(view)
|
||||
assert html =~ "Showing 1-10 of 15"
|
||||
end
|
||||
|
||||
test "pagination works with email filter", %{
|
||||
conn: conn,
|
||||
admin_user: admin_user,
|
||||
space: space,
|
||||
tomorrow: tomorrow
|
||||
} do
|
||||
user = register_user("user#{System.unique_integer([:positive])}@example.com", "Test User")
|
||||
|
||||
for i <- 1..15 do
|
||||
hour = rem(8 + i, 24)
|
||||
start_time = Time.new!(hour, 0, 0)
|
||||
end_time = Time.new!(hour, 30, 0)
|
||||
|
||||
BookingSystem.create_booking(
|
||||
space.id,
|
||||
user.id,
|
||||
tomorrow,
|
||||
start_time,
|
||||
end_time,
|
||||
"Customer #{i}",
|
||||
"customer#{i}@example.com",
|
||||
nil,
|
||||
nil
|
||||
)
|
||||
end
|
||||
|
||||
conn = log_in_user(conn, admin_user)
|
||||
{:ok, view, _html} = live(conn, "/admin/bookings")
|
||||
|
||||
view
|
||||
|> form("form", %{"email" => "customer1@example.com"})
|
||||
|> render_change()
|
||||
|
||||
html = render(view)
|
||||
assert html =~ "Showing 1-1 of 1"
|
||||
end
|
||||
|
||||
test "pagination works with date filter", %{
|
||||
conn: conn,
|
||||
admin_user: admin_user,
|
||||
space: space,
|
||||
tomorrow: tomorrow
|
||||
} do
|
||||
user = register_user("user#{System.unique_integer([:positive])}@example.com", "Test User")
|
||||
|
||||
for i <- 1..15 do
|
||||
hour = rem(8 + i, 24)
|
||||
start_time = Time.new!(hour, 0, 0)
|
||||
end_time = Time.new!(hour, 30, 0)
|
||||
|
||||
BookingSystem.create_booking(
|
||||
space.id,
|
||||
user.id,
|
||||
tomorrow,
|
||||
start_time,
|
||||
end_time,
|
||||
"Customer #{i}",
|
||||
"customer#{i}@example.com",
|
||||
nil,
|
||||
nil
|
||||
)
|
||||
end
|
||||
|
||||
conn = log_in_user(conn, admin_user)
|
||||
{:ok, view, _html} = live(conn, "/admin/bookings")
|
||||
|
||||
view
|
||||
|> form("form", %{"date" => Date.to_iso8601(tomorrow)})
|
||||
|> render_change()
|
||||
|
||||
html = render(view)
|
||||
assert html =~ "Showing 1-10 of 15"
|
||||
end
|
||||
|
||||
test "clear filters resets pagination", %{
|
||||
conn: conn,
|
||||
admin_user: admin_user,
|
||||
space: space,
|
||||
tomorrow: tomorrow
|
||||
} do
|
||||
user = register_user("user#{System.unique_integer([:positive])}@example.com", "Test User")
|
||||
|
||||
for i <- 1..15 do
|
||||
hour = rem(8 + i, 24)
|
||||
start_time = Time.new!(hour, 0, 0)
|
||||
end_time = Time.new!(hour, 30, 0)
|
||||
|
||||
BookingSystem.create_booking(
|
||||
space.id,
|
||||
user.id,
|
||||
tomorrow,
|
||||
start_time,
|
||||
end_time,
|
||||
"Customer #{i}",
|
||||
"customer#{i}@example.com",
|
||||
nil,
|
||||
nil
|
||||
)
|
||||
end
|
||||
|
||||
conn = log_in_user(conn, admin_user)
|
||||
{:ok, view, _html} = live(conn, "/admin/bookings?pending_page=2")
|
||||
|
||||
view
|
||||
|> element("button[phx-click='clear_filters']")
|
||||
|> render_click()
|
||||
|
||||
path = assert_patch(view)
|
||||
refute path =~ "pending_page=2"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -164,7 +164,11 @@ defmodule SpazioSolazzoWeb.BookingLive.SpaceBookingTest do
|
|||
|
||||
describe "SpaceBooking date selection" do
|
||||
test "updates time slots when selecting a different date", %{conn: conn, space: space} do
|
||||
monday_date = ~D[2026-02-02]
|
||||
# Find next Monday from today
|
||||
today = Date.utc_today()
|
||||
days_until_monday = rem(8 - Date.day_of_week(today), 7)
|
||||
days_until_monday = if days_until_monday == 0, do: 7, else: days_until_monday
|
||||
monday_date = Date.add(today, days_until_monday)
|
||||
|
||||
{:ok, _monday_slot} =
|
||||
BookingSystem.create_time_slot_template(
|
||||
|
|
@ -394,7 +398,17 @@ defmodule SpazioSolazzoWeb.BookingLive.SpaceBookingTest do
|
|||
# Process the handle_info message that creates the booking
|
||||
render(view)
|
||||
|
||||
{:ok, bookings} = BookingSystem.admin_search_bookings(space.id, nil, today)
|
||||
day_start = DateTime.new!(today, ~T[00:00:00], "Etc/UTC")
|
||||
day_end = DateTime.new!(today, ~T[23:59:59], "Etc/UTC")
|
||||
|
||||
{:ok, bookings} =
|
||||
BookingSystem.search_bookings(
|
||||
space.id,
|
||||
day_start,
|
||||
day_end,
|
||||
[:requested],
|
||||
[:customer_name, :customer_email, :customer_phone, :customer_comment, :state]
|
||||
)
|
||||
|
||||
assert length(bookings) == 1
|
||||
booking = hd(bookings)
|
||||
|
|
@ -578,7 +592,17 @@ defmodule SpazioSolazzoWeb.BookingLive.SpaceBookingTest do
|
|||
# Process the handle_info message that creates the booking
|
||||
render(view)
|
||||
|
||||
{:ok, bookings} = BookingSystem.admin_search_bookings(space.id, nil, today)
|
||||
day_start = DateTime.new!(today, ~T[00:00:00], "Etc/UTC")
|
||||
day_end = DateTime.new!(today, ~T[23:59:59], "Etc/UTC")
|
||||
|
||||
{:ok, bookings} =
|
||||
BookingSystem.search_bookings(
|
||||
space.id,
|
||||
day_start,
|
||||
day_end,
|
||||
[:requested],
|
||||
[:customer_email, :user_id]
|
||||
)
|
||||
|
||||
assert length(bookings) == 1
|
||||
booking = hd(bookings)
|
||||
|
|
@ -632,7 +656,14 @@ defmodule SpazioSolazzoWeb.BookingLive.SpaceBookingTest do
|
|||
test "handles rapid date changes", %{conn: conn, space: space} do
|
||||
{:ok, view, _html} = live(conn, ~p"/book/space/#{space.slug}")
|
||||
|
||||
dates = [~D[2026-02-02], ~D[2026-02-03], ~D[2026-02-04], ~D[2026-02-02]]
|
||||
# Use future dates relative to today
|
||||
today = Date.utc_today()
|
||||
dates = [
|
||||
Date.add(today, 1),
|
||||
Date.add(today, 2),
|
||||
Date.add(today, 3),
|
||||
Date.add(today, 1)
|
||||
]
|
||||
|
||||
for date <- dates do
|
||||
view
|
||||
|
|
@ -641,7 +672,10 @@ defmodule SpazioSolazzoWeb.BookingLive.SpaceBookingTest do
|
|||
end
|
||||
|
||||
html = render(view)
|
||||
assert html =~ "Monday, February 02, 2026"
|
||||
# Verify the last selected date is shown (which is Date.add(today, 1))
|
||||
final_date = Date.add(today, 1)
|
||||
formatted_date = Calendar.strftime(final_date, "%A, %B %d, %Y")
|
||||
assert html =~ formatted_date
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
Loading…
Reference in a new issue