mirror of
https://codeberg.org/JasterV/spazio-solazzo.git
synced 2026-04-26 18:20:03 +00:00
feat: refactor space booking view to be more efficient
This commit is contained in:
parent
23bdfd508a
commit
7ff3adf730
14 changed files with 654 additions and 1552 deletions
|
|
@ -13,10 +13,6 @@ defmodule SpazioSolazzo.BookingSystem do
|
|||
define :create_space,
|
||||
action: :create,
|
||||
args: [:name, :slug, :description, :capacity]
|
||||
|
||||
define :check_availability,
|
||||
action: :check_availability,
|
||||
args: [:space_id, :date, :start_time, :end_time]
|
||||
end
|
||||
|
||||
resource SpazioSolazzo.BookingSystem.TimeSlotTemplate do
|
||||
|
|
@ -40,9 +36,9 @@ defmodule SpazioSolazzo.BookingSystem do
|
|||
|
||||
define :count_pending_requests, action: :count_pending_requests
|
||||
|
||||
define :get_slot_booking_counts,
|
||||
action: :get_slot_booking_counts,
|
||||
args: [:space_id, :date, :start_time, :end_time]
|
||||
define :list_bookings_by_datetime_range,
|
||||
action: :by_datetime_range_and_status,
|
||||
args: [:space_id, :user_id, :start_datetime, :end_datetime, :states]
|
||||
|
||||
define :create_booking,
|
||||
action: :create,
|
||||
|
|
|
|||
|
|
@ -26,6 +26,18 @@ defmodule SpazioSolazzo.BookingSystem.Booking do
|
|||
references do
|
||||
reference :user, on_delete: :nilify, index?: true
|
||||
end
|
||||
|
||||
custom_indexes do
|
||||
# Composite index for space + datetime range queries (most common pattern)
|
||||
index [:space_id, :start_datetime, :end_datetime]
|
||||
|
||||
# Composite index for space + state queries (filtering by status)
|
||||
index [:space_id, :state]
|
||||
|
||||
# Single indexes for datetime overlap queries
|
||||
index [:start_datetime]
|
||||
index [:end_datetime]
|
||||
end
|
||||
end
|
||||
|
||||
state_machine do
|
||||
|
|
@ -97,6 +109,49 @@ defmodule SpazioSolazzo.BookingSystem.Booking do
|
|||
filter expr(state == :requested)
|
||||
end
|
||||
|
||||
read :by_datetime_range_and_status do
|
||||
description "Fetch bookings within a date/time range with optional filters"
|
||||
|
||||
argument :space_id, :uuid, allow_nil?: true
|
||||
argument :user_id, :uuid, allow_nil?: true
|
||||
argument :start_datetime, :datetime, allow_nil?: false
|
||||
argument :end_datetime, :datetime, allow_nil?: false
|
||||
argument :states, {:array, :atom}, allow_nil?: true
|
||||
|
||||
prepare fn query, _ctx ->
|
||||
start_dt = Ash.Query.get_argument(query, :start_datetime)
|
||||
end_dt = Ash.Query.get_argument(query, :end_datetime)
|
||||
|
||||
# Base datetime overlap filter
|
||||
query =
|
||||
Ash.Query.filter(
|
||||
query,
|
||||
start_datetime < ^end_dt and end_datetime > ^start_dt
|
||||
)
|
||||
|
||||
# Optional space filter
|
||||
query =
|
||||
case Ash.Query.get_argument(query, :space_id) do
|
||||
nil -> query
|
||||
space_id -> Ash.Query.filter(query, space_id == ^space_id)
|
||||
end
|
||||
|
||||
# Optional user filter
|
||||
query =
|
||||
case Ash.Query.get_argument(query, :user_id) do
|
||||
nil -> query
|
||||
user_id -> Ash.Query.filter(query, user_id == ^user_id)
|
||||
end
|
||||
|
||||
# Optional states filter
|
||||
case Ash.Query.get_argument(query, :states) do
|
||||
nil -> query
|
||||
[] -> query
|
||||
states -> Ash.Query.filter(query, state in ^states)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
create :create do
|
||||
argument :space_id, :uuid, allow_nil?: false
|
||||
argument :user_id, :uuid, allow_nil?: true
|
||||
|
|
@ -366,52 +421,10 @@ defmodule SpazioSolazzo.BookingSystem.Booking do
|
|||
description "Delete a booking record"
|
||||
primary? true
|
||||
end
|
||||
|
||||
action :get_slot_booking_counts, :map do
|
||||
argument :space_id, :uuid, allow_nil?: true
|
||||
argument :date, :date, allow_nil?: false
|
||||
argument :start_time, :time, allow_nil?: false
|
||||
argument :end_time, :time, allow_nil?: false
|
||||
|
||||
run fn input, _context ->
|
||||
space_id = input.arguments.space_id
|
||||
date = input.arguments.date
|
||||
start_time = input.arguments.start_time
|
||||
end_time = input.arguments.end_time
|
||||
|
||||
start_datetime = DateTime.new!(date, start_time, "Etc/UTC")
|
||||
end_datetime = DateTime.new!(date, end_time, "Etc/UTC")
|
||||
|
||||
query =
|
||||
__MODULE__
|
||||
|> Ash.Query.filter(
|
||||
start_datetime < ^end_datetime and end_datetime > ^start_datetime and
|
||||
(state == :requested or state == :accepted)
|
||||
)
|
||||
|
||||
query =
|
||||
if space_id do
|
||||
Ash.Query.filter(query, space_id == ^space_id)
|
||||
else
|
||||
query
|
||||
end
|
||||
|
||||
case Ash.read(query) do
|
||||
{:ok, bookings} ->
|
||||
pending_count = Enum.count(bookings, &(&1.state == :requested))
|
||||
approved_count = Enum.count(bookings, &(&1.state == :accepted))
|
||||
|
||||
{:ok, %{pending: pending_count, approved: approved_count}}
|
||||
|
||||
error ->
|
||||
error
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
policies do
|
||||
policy action([:cancel, :approve, :reject, :get_slot_booking_counts]) do
|
||||
policy action([:cancel, :approve, :reject]) do
|
||||
authorize_if always()
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -30,66 +30,9 @@ defmodule SpazioSolazzo.BookingSystem.Space do
|
|||
end
|
||||
end
|
||||
end
|
||||
|
||||
action :check_availability, :atom do
|
||||
argument :space_id, :uuid, allow_nil?: false
|
||||
argument :date, :date, allow_nil?: false
|
||||
argument :start_time, :time, allow_nil?: false
|
||||
argument :end_time, :time, allow_nil?: false
|
||||
|
||||
run fn input, _context ->
|
||||
require Ash.Query
|
||||
|
||||
space_id = input.arguments.space_id
|
||||
date_arg = input.arguments.date
|
||||
start_time_arg = input.arguments.start_time
|
||||
end_time_arg = input.arguments.end_time
|
||||
|
||||
start_datetime = DateTime.new!(date_arg, start_time_arg, "Etc/UTC")
|
||||
end_datetime = DateTime.new!(date_arg, end_time_arg, "Etc/UTC")
|
||||
|
||||
# Load the space
|
||||
case Ash.get(__MODULE__, space_id) do
|
||||
{:ok, space} ->
|
||||
# Get accepted bookings for this space that overlap with the requested time slot
|
||||
query =
|
||||
SpazioSolazzo.BookingSystem.Booking
|
||||
|> Ash.Query.filter(
|
||||
expr(
|
||||
space_id == ^space_id and state == :accepted and start_datetime < ^end_datetime and
|
||||
end_datetime > ^start_datetime
|
||||
)
|
||||
)
|
||||
|
||||
case Ash.read(query) do
|
||||
{:ok, overlapping_bookings} ->
|
||||
current_count = length(overlapping_bookings)
|
||||
|
||||
availability =
|
||||
if current_count >= space.capacity do
|
||||
:over_capacity
|
||||
else
|
||||
:available
|
||||
end
|
||||
|
||||
{:ok, availability}
|
||||
|
||||
error ->
|
||||
error
|
||||
end
|
||||
|
||||
error ->
|
||||
error
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
policies do
|
||||
policy action(:check_availability) do
|
||||
authorize_if always()
|
||||
end
|
||||
|
||||
policy action_type(:read) do
|
||||
authorize_if always()
|
||||
end
|
||||
|
|
|
|||
|
|
@ -67,4 +67,17 @@ defmodule SpazioSolazzo.BookingSystem.TimeSlotTemplate do
|
|||
public? true
|
||||
end
|
||||
end
|
||||
|
||||
calculations do
|
||||
calculate :booking_stats,
|
||||
:map,
|
||||
{SpazioSolazzo.BookingSystem.TimeSlotTemplate.Calculations.SlotBookingStats, []} do
|
||||
description "Calculates booking counts and availability for this time slot on a specific date"
|
||||
|
||||
argument :date, :date, allow_nil?: false
|
||||
argument :space_id, :uuid, allow_nil?: false
|
||||
argument :capacity, :integer, allow_nil?: false
|
||||
argument :user_id, :uuid, allow_nil?: true
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -0,0 +1,66 @@
|
|||
defmodule SpazioSolazzo.BookingSystem.TimeSlotTemplate.Calculations.SlotBookingStats do
|
||||
@moduledoc """
|
||||
Calculates booking statistics for time slots by fetching all bookings for the day once,
|
||||
then filtering in memory. This eliminates N+1 query problems.
|
||||
"""
|
||||
|
||||
use Ash.Resource.Calculation
|
||||
|
||||
@impl true
|
||||
def load(_query, _opts, _context) do
|
||||
[:start_time, :end_time, :space_id]
|
||||
end
|
||||
|
||||
@impl true
|
||||
def calculate(records, _opts, %{arguments: arguments}) do
|
||||
date = Map.get(arguments, :date)
|
||||
space_id = Map.get(arguments, :space_id)
|
||||
capacity = Map.get(arguments, :capacity)
|
||||
user_id = Map.get(arguments, :user_id)
|
||||
|
||||
# Fetch all bookings for the entire day ONCE
|
||||
day_start = DateTime.new!(date, ~T[00:00:00], "Etc/UTC")
|
||||
day_end = DateTime.new!(date, ~T[23:59:59], "Etc/UTC")
|
||||
|
||||
{:ok, all_bookings} =
|
||||
SpazioSolazzo.BookingSystem.list_bookings_by_datetime_range(
|
||||
space_id,
|
||||
nil,
|
||||
day_start,
|
||||
day_end,
|
||||
[:requested, :accepted]
|
||||
)
|
||||
|
||||
# Calculate stats for each slot using the cached bookings
|
||||
Enum.map(records, fn slot ->
|
||||
slot_start = DateTime.new!(date, slot.start_time, "Etc/UTC")
|
||||
slot_end = DateTime.new!(date, slot.end_time, "Etc/UTC")
|
||||
|
||||
# Filter bookings that overlap with this slot
|
||||
overlapping =
|
||||
Enum.filter(all_bookings, fn booking ->
|
||||
DateTime.compare(booking.start_datetime, slot_end) == :lt and
|
||||
DateTime.compare(booking.end_datetime, slot_start) == :gt
|
||||
end)
|
||||
|
||||
requested_count = Enum.count(overlapping, &(&1.state == :requested))
|
||||
accepted_count = Enum.count(overlapping, &(&1.state == :accepted))
|
||||
|
||||
user_has_booking =
|
||||
if user_id do
|
||||
Enum.any?(overlapping, &(&1.user_id == user_id))
|
||||
else
|
||||
false
|
||||
end
|
||||
|
||||
availability = if accepted_count >= capacity, do: :over_capacity, else: :available
|
||||
|
||||
%{
|
||||
requested_count: requested_count,
|
||||
accepted_count: accepted_count,
|
||||
user_has_booking: user_has_booking,
|
||||
availability_status: availability
|
||||
}
|
||||
end)
|
||||
end
|
||||
end
|
||||
|
|
@ -153,20 +153,31 @@ defmodule SpazioSolazzoWeb.Admin.WalkInLive do
|
|||
assign(socket, time_slot_warning: nil)
|
||||
else
|
||||
date = socket.assigns.selected_date
|
||||
start_time = socket.assigns.start_time
|
||||
end_time = socket.assigns.end_time
|
||||
space_id = socket.assigns.coworking_space.id
|
||||
capacity = socket.assigns.coworking_space.capacity
|
||||
|
||||
case BookingSystem.check_availability(
|
||||
socket.assigns.coworking_space.id,
|
||||
date,
|
||||
socket.assigns.start_time,
|
||||
socket.assigns.end_time
|
||||
) do
|
||||
{:ok, :over_capacity} ->
|
||||
assign(socket,
|
||||
time_slot_warning: "This time slot is currently overbooked. Proceed with caution."
|
||||
)
|
||||
start_datetime = DateTime.new!(date, start_time, "Etc/UTC")
|
||||
end_datetime = DateTime.new!(date, end_time, "Etc/UTC")
|
||||
|
||||
_ ->
|
||||
assign(socket, time_slot_warning: nil)
|
||||
{:ok, bookings} =
|
||||
BookingSystem.list_bookings_by_datetime_range(
|
||||
space_id,
|
||||
nil,
|
||||
start_datetime,
|
||||
end_datetime,
|
||||
[:accepted]
|
||||
)
|
||||
|
||||
accepted_count = length(bookings)
|
||||
|
||||
if accepted_count >= capacity do
|
||||
assign(socket,
|
||||
time_slot_warning: "This time slot is currently overbooked. Proceed with caution."
|
||||
)
|
||||
else
|
||||
assign(socket, time_slot_warning: nil)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -8,12 +8,9 @@ defmodule SpazioSolazzoWeb.SpaceBookingLive do
|
|||
case BookingSystem.get_space_by_slug(space_slug) do
|
||||
{:ok, space} ->
|
||||
selected_date = Date.utc_today()
|
||||
current_user = socket.assigns[:current_user]
|
||||
|
||||
{:ok, time_slots} =
|
||||
BookingSystem.get_space_time_slots_by_date(space.id, selected_date)
|
||||
|
||||
{:ok, bookings} =
|
||||
BookingSystem.list_accepted_space_bookings_by_date(space.id, selected_date)
|
||||
time_slots = load_time_slots_with_stats(space, selected_date, current_user)
|
||||
|
||||
if connected?(socket) do
|
||||
Phoenix.PubSub.subscribe(SpazioSolazzo.PubSub, "booking:created")
|
||||
|
|
@ -26,20 +23,13 @@ defmodule SpazioSolazzoWeb.SpaceBookingLive do
|
|||
socket
|
||||
|> assign(
|
||||
space: space,
|
||||
bookings: bookings,
|
||||
selected_date: selected_date,
|
||||
selected_time_slot: nil,
|
||||
show_booking_modal: false,
|
||||
show_success_modal: false,
|
||||
time_slots: time_slots,
|
||||
current_scope: nil,
|
||||
slot_availability: %{},
|
||||
slot_booking_counts: %{},
|
||||
user_booked_slots: %{}
|
||||
)
|
||||
|> compute_slot_availability()
|
||||
|> compute_slot_booking_counts()
|
||||
|> compute_user_booked_slots()}
|
||||
current_scope: nil
|
||||
)}
|
||||
|
||||
{:error, _error} ->
|
||||
{:ok,
|
||||
|
|
@ -53,7 +43,7 @@ defmodule SpazioSolazzoWeb.SpaceBookingLive do
|
|||
time_slot = Enum.find(socket.assigns.time_slots, &(&1.id == time_slot_id))
|
||||
|
||||
# Prevent opening modal if user already has a booking for this slot
|
||||
if socket.assigns.user_booked_slots[time_slot_id] do
|
||||
if time_slot && time_slot.booking_stats.user_has_booking do
|
||||
{:noreply, socket}
|
||||
else
|
||||
{:noreply,
|
||||
|
|
@ -99,13 +89,11 @@ defmodule SpazioSolazzoWeb.SpaceBookingLive do
|
|||
|
||||
{:error, %Ash.Error.Invalid{errors: errors}} ->
|
||||
error_message =
|
||||
errors
|
||||
|> Enum.map(fn
|
||||
Enum.map_join(errors, ", ", fn
|
||||
%{field: :date, message: msg} -> msg
|
||||
%{message: msg} -> msg
|
||||
_error -> "Invalid booking request"
|
||||
end)
|
||||
|> Enum.join(", ")
|
||||
|
||||
{:noreply,
|
||||
socket
|
||||
|
|
@ -121,111 +109,43 @@ defmodule SpazioSolazzoWeb.SpaceBookingLive do
|
|||
end
|
||||
|
||||
def handle_info({:date_selected, date}, socket) do
|
||||
{:ok, time_slots} =
|
||||
BookingSystem.get_space_time_slots_by_date(socket.assigns.space.id, date)
|
||||
|
||||
{:ok, bookings} =
|
||||
BookingSystem.list_accepted_space_bookings_by_date(socket.assigns.space.id, date)
|
||||
current_user = socket.assigns[:current_user]
|
||||
time_slots = load_time_slots_with_stats(socket.assigns.space, date, current_user)
|
||||
|
||||
{:noreply,
|
||||
socket
|
||||
|> assign(
|
||||
selected_date: date,
|
||||
time_slots: time_slots,
|
||||
bookings: bookings
|
||||
)
|
||||
|> compute_slot_availability()
|
||||
|> compute_slot_booking_counts()
|
||||
|> compute_user_booked_slots()}
|
||||
time_slots: time_slots
|
||||
)}
|
||||
end
|
||||
|
||||
def handle_info(
|
||||
%{topic: "booking:" <> _event, payload: %{data: %{space_id: space_id, date: date}}},
|
||||
%{assigns: %{space: %{id: space_id}, selected_date: date}} = socket
|
||||
) do
|
||||
{:ok, bookings} = BookingSystem.list_accepted_space_bookings_by_date(space_id, date)
|
||||
current_user = socket.assigns[:current_user]
|
||||
time_slots = load_time_slots_with_stats(socket.assigns.space, date, current_user)
|
||||
|
||||
{:noreply,
|
||||
socket
|
||||
|> assign(bookings: bookings)
|
||||
|> compute_slot_availability()
|
||||
|> compute_slot_booking_counts()
|
||||
|> compute_user_booked_slots()}
|
||||
|> assign(time_slots: time_slots)}
|
||||
end
|
||||
|
||||
def handle_info(_msg, socket) do
|
||||
{:noreply, socket}
|
||||
end
|
||||
|
||||
defp compute_slot_availability(socket) do
|
||||
slot_availability =
|
||||
socket.assigns.time_slots
|
||||
|> Enum.map(fn time_slot ->
|
||||
{:ok, status} =
|
||||
BookingSystem.check_availability(
|
||||
socket.assigns.space.id,
|
||||
socket.assigns.selected_date,
|
||||
time_slot.start_time,
|
||||
time_slot.end_time
|
||||
)
|
||||
defp load_time_slots_with_stats(space, date, current_user) do
|
||||
{:ok, time_slots} = BookingSystem.get_space_time_slots_by_date(space.id, date)
|
||||
|
||||
{time_slot.id, status}
|
||||
end)
|
||||
|> Map.new()
|
||||
|
||||
assign(socket, slot_availability: slot_availability)
|
||||
end
|
||||
|
||||
defp compute_slot_booking_counts(socket) do
|
||||
slot_booking_counts =
|
||||
socket.assigns.time_slots
|
||||
|> Enum.map(fn time_slot ->
|
||||
{:ok, counts} =
|
||||
BookingSystem.get_slot_booking_counts(
|
||||
socket.assigns.space.id,
|
||||
socket.assigns.selected_date,
|
||||
time_slot.start_time,
|
||||
time_slot.end_time
|
||||
)
|
||||
|
||||
{time_slot.id, counts}
|
||||
end)
|
||||
|> Map.new()
|
||||
|
||||
assign(socket, slot_booking_counts: slot_booking_counts)
|
||||
end
|
||||
|
||||
defp compute_user_booked_slots(socket) do
|
||||
current_user = socket.assigns.current_user
|
||||
|
||||
user_booked_slots =
|
||||
if current_user do
|
||||
socket.assigns.time_slots
|
||||
|> Enum.map(fn time_slot ->
|
||||
start_datetime =
|
||||
DateTime.new!(socket.assigns.selected_date, time_slot.start_time, "Etc/UTC")
|
||||
|
||||
end_datetime =
|
||||
DateTime.new!(socket.assigns.selected_date, time_slot.end_time, "Etc/UTC")
|
||||
|
||||
existing_bookings =
|
||||
SpazioSolazzo.BookingSystem.Booking
|
||||
|> Ash.Query.filter(
|
||||
user_id == ^current_user.id and
|
||||
space_id == ^socket.assigns.space.id and
|
||||
(state == :requested or state == :accepted) and
|
||||
start_datetime < ^end_datetime and
|
||||
end_datetime > ^start_datetime
|
||||
)
|
||||
|> Ash.read!()
|
||||
|
||||
{time_slot.id, existing_bookings != []}
|
||||
end)
|
||||
|> Map.new()
|
||||
else
|
||||
%{}
|
||||
end
|
||||
|
||||
assign(socket, user_booked_slots: user_booked_slots)
|
||||
Ash.load!(time_slots,
|
||||
booking_stats: %{
|
||||
date: date,
|
||||
space_id: space.id,
|
||||
capacity: space.capacity,
|
||||
user_id: current_user && current_user.id
|
||||
}
|
||||
)
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -45,83 +45,83 @@
|
|||
</div>
|
||||
<% else %>
|
||||
<%= for time_slot <- @time_slots do %>
|
||||
<% availability = Map.get(@slot_availability, time_slot.id, :available) %>
|
||||
<% counts =
|
||||
Map.get(@slot_booking_counts, time_slot.id, %{pending: 0, approved: 0}) %>
|
||||
<% user_has_booking = Map.get(@user_booked_slots, time_slot.id, false) %>
|
||||
<% availability = time_slot.booking_stats.availability_status %>
|
||||
<% requested_count = time_slot.booking_stats.requested_count %>
|
||||
<% accepted_count = time_slot.booking_stats.accepted_count %>
|
||||
<% user_has_booking = time_slot.booking_stats.user_has_booking %>
|
||||
<button
|
||||
phx-click={if user_has_booking, do: nil, else: "select_slot"}
|
||||
phx-value-time_slot_id={time_slot.id}
|
||||
disabled={user_has_booking}
|
||||
class={[
|
||||
"w-full p-4 rounded-xl border-2 transition-all duration-200 text-left",
|
||||
if(user_has_booking,
|
||||
do:
|
||||
"border-slate-200 bg-slate-100 cursor-not-allowed opacity-60 dark:bg-slate-700/50 dark:border-slate-600",
|
||||
else:
|
||||
if(availability == :available,
|
||||
do:
|
||||
"border-green-200 bg-green-50 hover:border-green-500 hover:shadow-lg cursor-pointer dark:bg-green-900/20 dark:border-green-800 dark:hover:border-green-600",
|
||||
else:
|
||||
"border-yellow-200 bg-yellow-50 hover:border-yellow-500 hover:shadow-lg cursor-pointer dark:bg-yellow-900/20 dark:border-yellow-800 dark:hover:border-yellow-600"
|
||||
)
|
||||
)
|
||||
]}
|
||||
>
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex-1">
|
||||
<div class="text-lg font-semibold text-slate-900 dark:text-white">
|
||||
{Calendar.strftime(time_slot.start_time, "%H:%M")} - {Calendar.strftime(
|
||||
time_slot.end_time,
|
||||
"%H:%M"
|
||||
)}
|
||||
phx-click={if user_has_booking, do: nil, else: "select_slot"}
|
||||
phx-value-time_slot_id={time_slot.id}
|
||||
disabled={user_has_booking}
|
||||
class={[
|
||||
"w-full p-4 rounded-xl border-2 transition-all duration-200 text-left",
|
||||
if(user_has_booking,
|
||||
do:
|
||||
"border-slate-200 bg-slate-100 cursor-not-allowed opacity-60 dark:bg-slate-700/50 dark:border-slate-600",
|
||||
else:
|
||||
if(availability == :available,
|
||||
do:
|
||||
"border-green-200 bg-green-50 hover:border-green-500 hover:shadow-lg cursor-pointer dark:bg-green-900/20 dark:border-green-800 dark:hover:border-green-600",
|
||||
else:
|
||||
"border-yellow-200 bg-yellow-50 hover:border-yellow-500 hover:shadow-lg cursor-pointer dark:bg-yellow-900/20 dark:border-yellow-800 dark:hover:border-yellow-600"
|
||||
)
|
||||
)
|
||||
]}
|
||||
>
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex-1">
|
||||
<div class="text-lg font-semibold text-slate-900 dark:text-white">
|
||||
{Calendar.strftime(time_slot.start_time, "%H:%M")} - {Calendar.strftime(
|
||||
time_slot.end_time,
|
||||
"%H:%M"
|
||||
)}
|
||||
</div>
|
||||
<%= if user_has_booking do %>
|
||||
<div class="text-sm text-slate-500 dark:text-slate-400 font-medium mt-1">
|
||||
Already Requested
|
||||
</div>
|
||||
<%= if user_has_booking do %>
|
||||
<div class="text-sm text-slate-500 dark:text-slate-400 font-medium mt-1">
|
||||
Already Requested
|
||||
<% else %>
|
||||
<%= if availability == :available do %>
|
||||
<div class="text-sm text-green-600 dark:text-green-400 font-medium mt-1">
|
||||
Available - Request Booking
|
||||
</div>
|
||||
<% else %>
|
||||
<%= if availability == :available do %>
|
||||
<div class="text-sm text-green-600 dark:text-green-400 font-medium mt-1">
|
||||
Available - Request Booking
|
||||
</div>
|
||||
<% else %>
|
||||
<div class="text-sm text-yellow-600 dark:text-yellow-400 font-medium mt-1">
|
||||
High Demand - Join Waitlist
|
||||
</div>
|
||||
<% end %>
|
||||
<div class="text-sm text-yellow-600 dark:text-yellow-400 font-medium mt-1">
|
||||
High Demand - Join Waitlist
|
||||
</div>
|
||||
<% end %>
|
||||
<% end %>
|
||||
<div class="flex gap-3 mt-2 text-xs text-slate-600 dark:text-slate-400">
|
||||
<%= if requested_count > 0 do %>
|
||||
<span class="flex items-center gap-1">
|
||||
<.icon name="hero-clock" class="w-3.5 h-3.5" />
|
||||
{requested_count} pending
|
||||
</span>
|
||||
<% end %>
|
||||
<%= if accepted_count > 0 do %>
|
||||
<span class="flex items-center gap-1">
|
||||
<.icon name="hero-check-circle" class="w-3.5 h-3.5" />
|
||||
{accepted_count} booked
|
||||
</span>
|
||||
<% end %>
|
||||
<div class="flex gap-3 mt-2 text-xs text-slate-600 dark:text-slate-400">
|
||||
<%= if counts.pending > 0 do %>
|
||||
<span class="flex items-center gap-1">
|
||||
<.icon name="hero-clock" class="w-3.5 h-3.5" />
|
||||
{counts.pending} pending
|
||||
</span>
|
||||
<% end %>
|
||||
<%= if counts.approved > 0 do %>
|
||||
<span class="flex items-center gap-1">
|
||||
<.icon name="hero-check-circle" class="w-3.5 h-3.5" />
|
||||
{counts.approved} booked
|
||||
</span>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
<.icon
|
||||
name={if user_has_booking, do: "hero-check", else: "hero-arrow-right"}
|
||||
class={[
|
||||
"w-5 h-5",
|
||||
if(user_has_booking,
|
||||
do: "text-slate-400 dark:text-slate-500",
|
||||
else:
|
||||
if(availability == :available,
|
||||
do: "text-green-500 dark:text-green-400",
|
||||
else: "text-yellow-500 dark:text-yellow-400"
|
||||
)
|
||||
)
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
</button>
|
||||
<.icon
|
||||
name={if user_has_booking, do: "hero-check", else: "hero-arrow-right"}
|
||||
class={[
|
||||
"w-5 h-5",
|
||||
if(user_has_booking,
|
||||
do: "text-slate-400 dark:text-slate-500",
|
||||
else:
|
||||
if(availability == :available,
|
||||
do: "text-green-500 dark:text-green-400",
|
||||
else: "text-yellow-500 dark:text-yellow-400"
|
||||
)
|
||||
)
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
</button>
|
||||
<% end %>
|
||||
<% end %>
|
||||
</div>
|
||||
|
|
@ -146,7 +146,7 @@
|
|||
current_user={@current_user}
|
||||
slot_availability={
|
||||
if @selected_time_slot do
|
||||
Map.get(@slot_availability, @selected_time_slot.id, :available)
|
||||
@selected_time_slot.booking_stats.availability_status
|
||||
else
|
||||
:available
|
||||
end
|
||||
|
|
|
|||
29
priv/repo/migrations/20260202155008_add_booking_indexes.exs
Normal file
29
priv/repo/migrations/20260202155008_add_booking_indexes.exs
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
defmodule SpazioSolazzo.Repo.Migrations.AddBookingIndexes do
|
||||
@moduledoc """
|
||||
Updates resources based on their most recent snapshots.
|
||||
|
||||
This file was autogenerated with `mix ash_postgres.generate_migrations`
|
||||
"""
|
||||
|
||||
use Ecto.Migration
|
||||
|
||||
def up do
|
||||
create index(:bookings, [:end_datetime])
|
||||
|
||||
create index(:bookings, [:start_datetime])
|
||||
|
||||
create index(:bookings, [:space_id, :state])
|
||||
|
||||
create index(:bookings, [:space_id, :start_datetime, :end_datetime])
|
||||
end
|
||||
|
||||
def down do
|
||||
drop_if_exists index(:bookings, [:space_id, :start_datetime, :end_datetime])
|
||||
|
||||
drop_if_exists index(:bookings, [:space_id, :state])
|
||||
|
||||
drop_if_exists index(:bookings, [:start_datetime])
|
||||
|
||||
drop_if_exists index(:bookings, [:end_datetime])
|
||||
end
|
||||
end
|
||||
365
priv/resource_snapshots/repo/bookings/20260202155008.json
Normal file
365
priv/resource_snapshots/repo/bookings/20260202155008.json
Normal file
|
|
@ -0,0 +1,365 @@
|
|||
{
|
||||
"attributes": [
|
||||
{
|
||||
"allow_nil?": false,
|
||||
"default": "fragment(\"gen_random_uuid()\")",
|
||||
"generated?": false,
|
||||
"precision": null,
|
||||
"primary_key?": true,
|
||||
"references": null,
|
||||
"scale": null,
|
||||
"size": null,
|
||||
"source": "id",
|
||||
"type": "uuid"
|
||||
},
|
||||
{
|
||||
"allow_nil?": false,
|
||||
"default": "nil",
|
||||
"generated?": false,
|
||||
"precision": null,
|
||||
"primary_key?": false,
|
||||
"references": null,
|
||||
"scale": null,
|
||||
"size": null,
|
||||
"source": "start_datetime",
|
||||
"type": "utc_datetime"
|
||||
},
|
||||
{
|
||||
"allow_nil?": false,
|
||||
"default": "nil",
|
||||
"generated?": false,
|
||||
"precision": null,
|
||||
"primary_key?": false,
|
||||
"references": null,
|
||||
"scale": null,
|
||||
"size": null,
|
||||
"source": "end_datetime",
|
||||
"type": "utc_datetime"
|
||||
},
|
||||
{
|
||||
"allow_nil?": false,
|
||||
"default": "nil",
|
||||
"generated?": false,
|
||||
"precision": null,
|
||||
"primary_key?": false,
|
||||
"references": null,
|
||||
"scale": null,
|
||||
"size": null,
|
||||
"source": "date",
|
||||
"type": "date"
|
||||
},
|
||||
{
|
||||
"allow_nil?": false,
|
||||
"default": "nil",
|
||||
"generated?": false,
|
||||
"precision": null,
|
||||
"primary_key?": false,
|
||||
"references": null,
|
||||
"scale": null,
|
||||
"size": null,
|
||||
"source": "customer_name",
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"allow_nil?": false,
|
||||
"default": "nil",
|
||||
"generated?": false,
|
||||
"precision": null,
|
||||
"primary_key?": false,
|
||||
"references": null,
|
||||
"scale": null,
|
||||
"size": null,
|
||||
"source": "customer_email",
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"allow_nil?": false,
|
||||
"default": "nil",
|
||||
"generated?": false,
|
||||
"precision": null,
|
||||
"primary_key?": false,
|
||||
"references": null,
|
||||
"scale": null,
|
||||
"size": null,
|
||||
"source": "start_time",
|
||||
"type": "time"
|
||||
},
|
||||
{
|
||||
"allow_nil?": false,
|
||||
"default": "nil",
|
||||
"generated?": false,
|
||||
"precision": null,
|
||||
"primary_key?": false,
|
||||
"references": null,
|
||||
"scale": null,
|
||||
"size": null,
|
||||
"source": "end_time",
|
||||
"type": "time"
|
||||
},
|
||||
{
|
||||
"allow_nil?": true,
|
||||
"default": "nil",
|
||||
"generated?": false,
|
||||
"precision": null,
|
||||
"primary_key?": false,
|
||||
"references": null,
|
||||
"scale": null,
|
||||
"size": null,
|
||||
"source": "customer_phone",
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"allow_nil?": true,
|
||||
"default": "nil",
|
||||
"generated?": false,
|
||||
"precision": null,
|
||||
"primary_key?": false,
|
||||
"references": null,
|
||||
"scale": null,
|
||||
"size": null,
|
||||
"source": "customer_comment",
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"allow_nil?": true,
|
||||
"default": "nil",
|
||||
"generated?": false,
|
||||
"precision": null,
|
||||
"primary_key?": false,
|
||||
"references": null,
|
||||
"scale": null,
|
||||
"size": null,
|
||||
"source": "cancellation_reason",
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"allow_nil?": true,
|
||||
"default": "nil",
|
||||
"generated?": false,
|
||||
"precision": null,
|
||||
"primary_key?": false,
|
||||
"references": null,
|
||||
"scale": null,
|
||||
"size": null,
|
||||
"source": "rejection_reason",
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"allow_nil?": false,
|
||||
"default": "\"requested\"",
|
||||
"generated?": false,
|
||||
"precision": null,
|
||||
"primary_key?": false,
|
||||
"references": null,
|
||||
"scale": null,
|
||||
"size": null,
|
||||
"source": "state",
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"allow_nil?": false,
|
||||
"default": "fragment(\"(now() AT TIME ZONE 'utc')\")",
|
||||
"generated?": false,
|
||||
"precision": null,
|
||||
"primary_key?": false,
|
||||
"references": null,
|
||||
"scale": null,
|
||||
"size": null,
|
||||
"source": "inserted_at",
|
||||
"type": "utc_datetime_usec"
|
||||
},
|
||||
{
|
||||
"allow_nil?": false,
|
||||
"default": "fragment(\"(now() AT TIME ZONE 'utc')\")",
|
||||
"generated?": false,
|
||||
"precision": null,
|
||||
"primary_key?": false,
|
||||
"references": null,
|
||||
"scale": null,
|
||||
"size": null,
|
||||
"source": "updated_at",
|
||||
"type": "utc_datetime_usec"
|
||||
},
|
||||
{
|
||||
"allow_nil?": false,
|
||||
"default": "nil",
|
||||
"generated?": false,
|
||||
"precision": null,
|
||||
"primary_key?": false,
|
||||
"references": {
|
||||
"deferrable": false,
|
||||
"destination_attribute": "id",
|
||||
"destination_attribute_default": null,
|
||||
"destination_attribute_generated": null,
|
||||
"index?": false,
|
||||
"match_type": null,
|
||||
"match_with": null,
|
||||
"multitenancy": {
|
||||
"attribute": null,
|
||||
"global": null,
|
||||
"strategy": null
|
||||
},
|
||||
"name": "bookings_space_id_fkey",
|
||||
"on_delete": null,
|
||||
"on_update": null,
|
||||
"primary_key?": true,
|
||||
"schema": "public",
|
||||
"table": "spaces"
|
||||
},
|
||||
"scale": null,
|
||||
"size": null,
|
||||
"source": "space_id",
|
||||
"type": "uuid"
|
||||
},
|
||||
{
|
||||
"allow_nil?": true,
|
||||
"default": "nil",
|
||||
"generated?": false,
|
||||
"precision": null,
|
||||
"primary_key?": false,
|
||||
"references": {
|
||||
"deferrable": false,
|
||||
"destination_attribute": "id",
|
||||
"destination_attribute_default": null,
|
||||
"destination_attribute_generated": null,
|
||||
"index?": true,
|
||||
"match_type": null,
|
||||
"match_with": null,
|
||||
"multitenancy": {
|
||||
"attribute": null,
|
||||
"global": null,
|
||||
"strategy": null
|
||||
},
|
||||
"name": "bookings_user_id_fkey",
|
||||
"on_delete": "nilify",
|
||||
"on_update": null,
|
||||
"primary_key?": true,
|
||||
"schema": "public",
|
||||
"table": "users"
|
||||
},
|
||||
"scale": null,
|
||||
"size": null,
|
||||
"source": "user_id",
|
||||
"type": "uuid"
|
||||
}
|
||||
],
|
||||
"base_filter": null,
|
||||
"check_constraints": [],
|
||||
"custom_indexes": [
|
||||
{
|
||||
"all_tenants?": false,
|
||||
"concurrently": false,
|
||||
"error_fields": [
|
||||
"space_id",
|
||||
"start_datetime",
|
||||
"end_datetime"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"type": "atom",
|
||||
"value": "space_id"
|
||||
},
|
||||
{
|
||||
"type": "atom",
|
||||
"value": "start_datetime"
|
||||
},
|
||||
{
|
||||
"type": "atom",
|
||||
"value": "end_datetime"
|
||||
}
|
||||
],
|
||||
"include": null,
|
||||
"message": null,
|
||||
"name": null,
|
||||
"nulls_distinct": true,
|
||||
"prefix": null,
|
||||
"table": null,
|
||||
"unique": false,
|
||||
"using": null,
|
||||
"where": null
|
||||
},
|
||||
{
|
||||
"all_tenants?": false,
|
||||
"concurrently": false,
|
||||
"error_fields": [
|
||||
"space_id",
|
||||
"state"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"type": "atom",
|
||||
"value": "space_id"
|
||||
},
|
||||
{
|
||||
"type": "atom",
|
||||
"value": "state"
|
||||
}
|
||||
],
|
||||
"include": null,
|
||||
"message": null,
|
||||
"name": null,
|
||||
"nulls_distinct": true,
|
||||
"prefix": null,
|
||||
"table": null,
|
||||
"unique": false,
|
||||
"using": null,
|
||||
"where": null
|
||||
},
|
||||
{
|
||||
"all_tenants?": false,
|
||||
"concurrently": false,
|
||||
"error_fields": [
|
||||
"start_datetime"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"type": "atom",
|
||||
"value": "start_datetime"
|
||||
}
|
||||
],
|
||||
"include": null,
|
||||
"message": null,
|
||||
"name": null,
|
||||
"nulls_distinct": true,
|
||||
"prefix": null,
|
||||
"table": null,
|
||||
"unique": false,
|
||||
"using": null,
|
||||
"where": null
|
||||
},
|
||||
{
|
||||
"all_tenants?": false,
|
||||
"concurrently": false,
|
||||
"error_fields": [
|
||||
"end_datetime"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"type": "atom",
|
||||
"value": "end_datetime"
|
||||
}
|
||||
],
|
||||
"include": null,
|
||||
"message": null,
|
||||
"name": null,
|
||||
"nulls_distinct": true,
|
||||
"prefix": null,
|
||||
"table": null,
|
||||
"unique": false,
|
||||
"using": null,
|
||||
"where": null
|
||||
}
|
||||
],
|
||||
"custom_statements": [],
|
||||
"has_create_action": true,
|
||||
"hash": "5DB2211C98FA47BC6D744D8FD763CC8920F067DFD092EC120CBD2115E26810CF",
|
||||
"identities": [],
|
||||
"multitenancy": {
|
||||
"attribute": null,
|
||||
"global": null,
|
||||
"strategy": null
|
||||
},
|
||||
"repo": "Elixir.SpazioSolazzo.Repo",
|
||||
"schema": null,
|
||||
"table": "bookings"
|
||||
}
|
||||
|
|
@ -576,195 +576,6 @@ defmodule SpazioSolazzo.BookingSystem.BookingTest do
|
|||
end
|
||||
end
|
||||
|
||||
describe "check_availability/4" do
|
||||
test "returns :available when under public capacity", %{space: space, date: date} do
|
||||
{:ok, booking} =
|
||||
request_booking(
|
||||
space.id,
|
||||
nil,
|
||||
date,
|
||||
~T[09:00:00],
|
||||
~T[10:00:00],
|
||||
"User 1",
|
||||
"user1@example.com",
|
||||
"",
|
||||
""
|
||||
)
|
||||
|
||||
{:ok, _} = BookingSystem.approve_booking(booking.id)
|
||||
|
||||
{:ok, status} =
|
||||
BookingSystem.check_availability(
|
||||
space.id,
|
||||
date,
|
||||
~T[09:00:00],
|
||||
~T[10:00:00]
|
||||
)
|
||||
|
||||
assert status == :available
|
||||
end
|
||||
|
||||
test "returns :over_capacity when at or over capacity", %{
|
||||
space: space,
|
||||
date: date
|
||||
} do
|
||||
for i <- 1..2 do
|
||||
{:ok, booking} =
|
||||
request_booking(
|
||||
space.id,
|
||||
nil,
|
||||
date,
|
||||
~T[09:00:00],
|
||||
~T[10:00:00],
|
||||
"User #{i}",
|
||||
"user#{i}@example.com",
|
||||
"",
|
||||
""
|
||||
)
|
||||
|
||||
{:ok, _} = BookingSystem.approve_booking(booking.id)
|
||||
end
|
||||
|
||||
{:ok, status} =
|
||||
BookingSystem.check_availability(
|
||||
space.id,
|
||||
date,
|
||||
~T[09:00:00],
|
||||
~T[10:00:00]
|
||||
)
|
||||
|
||||
assert status == :over_capacity
|
||||
end
|
||||
|
||||
test "only counts overlapping bookings", %{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, _} = BookingSystem.approve_booking(booking1.id)
|
||||
|
||||
{:ok, booking2} =
|
||||
request_booking(
|
||||
space.id,
|
||||
nil,
|
||||
date,
|
||||
~T[10:00:00],
|
||||
~T[11:00:00],
|
||||
"User 2",
|
||||
"user2@example.com",
|
||||
"",
|
||||
""
|
||||
)
|
||||
|
||||
{:ok, _} = BookingSystem.approve_booking(booking2.id)
|
||||
|
||||
{:ok, status} =
|
||||
BookingSystem.check_availability(
|
||||
space.id,
|
||||
date,
|
||||
~T[10:00:00],
|
||||
~T[11:00:00]
|
||||
)
|
||||
|
||||
assert status == :available
|
||||
end
|
||||
|
||||
test "counts partial overlaps", %{space: space, date: date} do
|
||||
for i <- 1..2 do
|
||||
{:ok, booking} =
|
||||
request_booking(
|
||||
space.id,
|
||||
nil,
|
||||
date,
|
||||
~T[09:00:00],
|
||||
~T[11:00:00],
|
||||
"User #{i}",
|
||||
"user#{i}@example.com",
|
||||
"",
|
||||
""
|
||||
)
|
||||
|
||||
{:ok, _} = BookingSystem.approve_booking(booking.id)
|
||||
end
|
||||
|
||||
{:ok, status} =
|
||||
BookingSystem.check_availability(
|
||||
space.id,
|
||||
date,
|
||||
~T[10:00:00],
|
||||
~T[12:00:00]
|
||||
)
|
||||
|
||||
assert status == :over_capacity
|
||||
end
|
||||
|
||||
test "does not count pending bookings", %{space: space, date: date} do
|
||||
for i <- 1..3 do
|
||||
{:ok, _booking} =
|
||||
request_booking(
|
||||
space.id,
|
||||
nil,
|
||||
date,
|
||||
~T[09:00:00],
|
||||
~T[10:00:00],
|
||||
"User #{i}",
|
||||
"user#{i}@example.com",
|
||||
"",
|
||||
""
|
||||
)
|
||||
end
|
||||
|
||||
{:ok, status} =
|
||||
BookingSystem.check_availability(
|
||||
space.id,
|
||||
date,
|
||||
~T[09:00:00],
|
||||
~T[10:00:00]
|
||||
)
|
||||
|
||||
assert status == :available
|
||||
end
|
||||
|
||||
test "does not count cancelled bookings", %{space: space, date: date} do
|
||||
for i <- 1..3 do
|
||||
{:ok, booking} =
|
||||
request_booking(
|
||||
space.id,
|
||||
nil,
|
||||
date,
|
||||
~T[09:00:00],
|
||||
~T[10:00:00],
|
||||
"User #{i}",
|
||||
"user#{i}@example.com",
|
||||
"",
|
||||
""
|
||||
)
|
||||
|
||||
{:ok, _} = BookingSystem.approve_booking(booking.id)
|
||||
{:ok, _} = BookingSystem.cancel_booking(booking.id, "Test cancellation")
|
||||
end
|
||||
|
||||
{:ok, status} =
|
||||
BookingSystem.check_availability(
|
||||
space.id,
|
||||
date,
|
||||
~T[09:00:00],
|
||||
~T[10:00:00]
|
||||
)
|
||||
|
||||
assert status == :available
|
||||
end
|
||||
end
|
||||
|
||||
describe "count_pending_requests/0" do
|
||||
test "returns only pending bookings", %{space: space, date: date} do
|
||||
{:ok, _pending1} =
|
||||
|
|
@ -1014,290 +825,5 @@ defmodule SpazioSolazzo.BookingSystem.BookingTest do
|
|||
assert booking.start_time == start_time
|
||||
assert booking.end_time == end_time
|
||||
end
|
||||
|
||||
test "walk-in bookings are counted in availability check", %{space: space} do
|
||||
date = Date.add(Date.utc_today(), 1)
|
||||
start_time = ~T[09:00:00]
|
||||
end_time = ~T[10:00:00]
|
||||
|
||||
for i <- 1..3 do
|
||||
BookingSystem.create_walk_in(
|
||||
space.id,
|
||||
DateTime.new!(date, start_time, "Etc/UTC"),
|
||||
DateTime.new!(date, end_time, "Etc/UTC"),
|
||||
"Customer #{i}",
|
||||
"customer#{i}@example.com",
|
||||
nil,
|
||||
nil
|
||||
)
|
||||
end
|
||||
|
||||
{:ok, status} = BookingSystem.check_availability(space.id, date, start_time, end_time)
|
||||
|
||||
assert status == :over_capacity
|
||||
end
|
||||
end
|
||||
|
||||
describe "get_slot_booking_counts/4" do
|
||||
test "counts pending and approved bookings in time slot", %{space: space, date: date} do
|
||||
{:ok, _pending1} =
|
||||
request_booking(
|
||||
space.id,
|
||||
nil,
|
||||
date,
|
||||
~T[09:00:00],
|
||||
~T[10:00:00],
|
||||
"User 1",
|
||||
"user1@example.com",
|
||||
"",
|
||||
""
|
||||
)
|
||||
|
||||
{:ok, approved1} =
|
||||
request_booking(
|
||||
space.id,
|
||||
nil,
|
||||
date,
|
||||
~T[09:00:00],
|
||||
~T[10:00:00],
|
||||
"User 2",
|
||||
"user2@example.com",
|
||||
"",
|
||||
""
|
||||
)
|
||||
|
||||
{:ok, _} = BookingSystem.approve_booking(approved1.id)
|
||||
|
||||
{:ok, approved2} =
|
||||
request_booking(
|
||||
space.id,
|
||||
nil,
|
||||
date,
|
||||
~T[09:00:00],
|
||||
~T[10:00:00],
|
||||
"User 3",
|
||||
"user3@example.com",
|
||||
"",
|
||||
""
|
||||
)
|
||||
|
||||
{:ok, _} = BookingSystem.approve_booking(approved2.id)
|
||||
|
||||
{:ok, counts} =
|
||||
BookingSystem.get_slot_booking_counts(space.id, date, ~T[09:00:00], ~T[10:00:00])
|
||||
|
||||
assert counts.pending == 1
|
||||
assert counts.approved == 2
|
||||
end
|
||||
|
||||
test "only counts overlapping bookings", %{space: space, date: date} do
|
||||
{:ok, _pending1} =
|
||||
request_booking(
|
||||
space.id,
|
||||
nil,
|
||||
date,
|
||||
~T[08:00:00],
|
||||
~T[09:00:00],
|
||||
"User 1",
|
||||
"user1@example.com",
|
||||
"",
|
||||
""
|
||||
)
|
||||
|
||||
{:ok, _pending2} =
|
||||
request_booking(
|
||||
space.id,
|
||||
nil,
|
||||
date,
|
||||
~T[10:00:00],
|
||||
~T[11:00:00],
|
||||
"User 2",
|
||||
"user2@example.com",
|
||||
"",
|
||||
""
|
||||
)
|
||||
|
||||
{:ok, counts} =
|
||||
BookingSystem.get_slot_booking_counts(space.id, date, ~T[09:00:00], ~T[10:00:00])
|
||||
|
||||
assert counts.pending == 0
|
||||
assert counts.approved == 0
|
||||
end
|
||||
|
||||
test "counts partial overlaps", %{space: space, date: date} do
|
||||
{:ok, _pending1} =
|
||||
request_booking(
|
||||
space.id,
|
||||
nil,
|
||||
date,
|
||||
~T[08:30:00],
|
||||
~T[09:30:00],
|
||||
"User 1",
|
||||
"user1@example.com",
|
||||
"",
|
||||
""
|
||||
)
|
||||
|
||||
{:ok, approved1} =
|
||||
request_booking(
|
||||
space.id,
|
||||
nil,
|
||||
date,
|
||||
~T[09:30:00],
|
||||
~T[10:30:00],
|
||||
"User 2",
|
||||
"user2@example.com",
|
||||
"",
|
||||
""
|
||||
)
|
||||
|
||||
{:ok, _} = BookingSystem.approve_booking(approved1.id)
|
||||
|
||||
{:ok, counts} =
|
||||
BookingSystem.get_slot_booking_counts(space.id, date, ~T[09:00:00], ~T[10:00:00])
|
||||
|
||||
assert counts.pending == 1
|
||||
assert counts.approved == 1
|
||||
end
|
||||
|
||||
test "does not count cancelled bookings", %{space: space, date: date} do
|
||||
{:ok, cancelled} =
|
||||
request_booking(
|
||||
space.id,
|
||||
nil,
|
||||
date,
|
||||
~T[09:00:00],
|
||||
~T[10:00:00],
|
||||
"User 1",
|
||||
"user1@example.com",
|
||||
"",
|
||||
""
|
||||
)
|
||||
|
||||
{:ok, _} = BookingSystem.cancel_booking(cancelled.id, "Test cancellation")
|
||||
|
||||
{:ok, counts} =
|
||||
BookingSystem.get_slot_booking_counts(space.id, date, ~T[09:00:00], ~T[10:00:00])
|
||||
|
||||
assert counts.pending == 0
|
||||
assert counts.approved == 0
|
||||
end
|
||||
|
||||
test "does not count rejected bookings", %{space: space, date: date} do
|
||||
{:ok, rejected} =
|
||||
request_booking(
|
||||
space.id,
|
||||
nil,
|
||||
date,
|
||||
~T[09:00:00],
|
||||
~T[10:00:00],
|
||||
"User 1",
|
||||
"user1@example.com",
|
||||
"",
|
||||
""
|
||||
)
|
||||
|
||||
{:ok, _} = BookingSystem.reject_booking(rejected.id, "Space not available")
|
||||
|
||||
{:ok, counts} =
|
||||
BookingSystem.get_slot_booking_counts(space.id, date, ~T[09:00:00], ~T[10:00:00])
|
||||
|
||||
assert counts.pending == 0
|
||||
assert counts.approved == 0
|
||||
end
|
||||
|
||||
test "filters by date correctly", %{space: space, date: date} do
|
||||
other_date = Date.add(date, 1)
|
||||
|
||||
{:ok, _pending1} =
|
||||
request_booking(
|
||||
space.id,
|
||||
nil,
|
||||
other_date,
|
||||
~T[09:00:00],
|
||||
~T[10:00:00],
|
||||
"User 1",
|
||||
"user1@example.com",
|
||||
"",
|
||||
""
|
||||
)
|
||||
|
||||
{:ok, counts} =
|
||||
BookingSystem.get_slot_booking_counts(space.id, date, ~T[09:00:00], ~T[10:00:00])
|
||||
|
||||
assert counts.pending == 0
|
||||
assert counts.approved == 0
|
||||
end
|
||||
|
||||
test "filters by space correctly", %{space: space, date: date} do
|
||||
{:ok, other_space} =
|
||||
BookingSystem.create_space(
|
||||
"Other Space",
|
||||
"other-space-counts",
|
||||
"Other description",
|
||||
5
|
||||
)
|
||||
|
||||
{:ok, _pending1} =
|
||||
request_booking(
|
||||
other_space.id,
|
||||
nil,
|
||||
date,
|
||||
~T[09:00:00],
|
||||
~T[10:00:00],
|
||||
"User 1",
|
||||
"user1@example.com",
|
||||
"",
|
||||
""
|
||||
)
|
||||
|
||||
{:ok, counts} =
|
||||
BookingSystem.get_slot_booking_counts(space.id, date, ~T[09:00:00], ~T[10:00:00])
|
||||
|
||||
assert counts.pending == 0
|
||||
assert counts.approved == 0
|
||||
end
|
||||
|
||||
test "works with nil space_id to count all spaces", %{space: space, date: date} do
|
||||
{:ok, other_space} =
|
||||
BookingSystem.create_space(
|
||||
"Other Space",
|
||||
"other-space-all",
|
||||
"Other description",
|
||||
5
|
||||
)
|
||||
|
||||
{:ok, _pending1} =
|
||||
request_booking(
|
||||
space.id,
|
||||
nil,
|
||||
date,
|
||||
~T[09:00:00],
|
||||
~T[10:00:00],
|
||||
"User 1",
|
||||
"user1@example.com",
|
||||
"",
|
||||
""
|
||||
)
|
||||
|
||||
{:ok, _pending2} =
|
||||
request_booking(
|
||||
other_space.id,
|
||||
nil,
|
||||
date,
|
||||
~T[09:00:00],
|
||||
~T[10:00:00],
|
||||
"User 2",
|
||||
"user2@example.com",
|
||||
"",
|
||||
""
|
||||
)
|
||||
|
||||
{:ok, counts} =
|
||||
BookingSystem.get_slot_booking_counts(nil, date, ~T[09:00:00], ~T[10:00:00])
|
||||
|
||||
assert counts.pending == 2
|
||||
assert counts.approved == 0
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -1,501 +0,0 @@
|
|||
defmodule SpazioSolazzo.BookingSystem.MultiDayBookingTest do
|
||||
@moduledoc """
|
||||
Tests for multi-day booking functionality using datetime fields.
|
||||
Verifies that bookings can span multiple days and that datetime range
|
||||
queries work correctly for availability checking and listing.
|
||||
"""
|
||||
|
||||
use SpazioSolazzo.DataCase, async: true
|
||||
|
||||
alias SpazioSolazzo.BookingSystem
|
||||
|
||||
setup do
|
||||
{:ok, space} =
|
||||
BookingSystem.create_space(
|
||||
"Coworking",
|
||||
"coworking",
|
||||
"Coworking space for testing",
|
||||
5
|
||||
)
|
||||
|
||||
%{space: space}
|
||||
end
|
||||
|
||||
describe "multi-day walk-in bookings" do
|
||||
test "can create a multi-day booking spanning 3 days", %{space: space} do
|
||||
start_date = Date.add(Date.utc_today(), 1)
|
||||
end_date = Date.add(Date.utc_today(), 3)
|
||||
|
||||
start_datetime = DateTime.new!(start_date, ~T[09:00:00], "Etc/UTC")
|
||||
end_datetime = DateTime.new!(end_date, ~T[18:00:00], "Etc/UTC")
|
||||
|
||||
{:ok, booking} =
|
||||
BookingSystem.create_walk_in(
|
||||
space.id,
|
||||
start_datetime,
|
||||
end_datetime,
|
||||
"John Doe",
|
||||
"john@example.com",
|
||||
nil,
|
||||
nil
|
||||
)
|
||||
|
||||
assert booking.start_datetime == start_datetime
|
||||
assert booking.end_datetime == end_datetime
|
||||
assert booking.state == :accepted
|
||||
assert booking.customer_name == "John Doe"
|
||||
end
|
||||
|
||||
test "multi-day booking appears in queries for all days it spans", %{space: space} do
|
||||
start_date = Date.add(Date.utc_today(), 1)
|
||||
end_date = Date.add(Date.utc_today(), 4)
|
||||
|
||||
start_datetime = DateTime.new!(start_date, ~T[10:00:00], "Etc/UTC")
|
||||
end_datetime = DateTime.new!(end_date, ~T[17:00:00], "Etc/UTC")
|
||||
|
||||
{:ok, _booking} =
|
||||
BookingSystem.create_walk_in(
|
||||
space.id,
|
||||
start_datetime,
|
||||
end_datetime,
|
||||
"Jane Smith",
|
||||
"jane@example.com",
|
||||
nil,
|
||||
nil
|
||||
)
|
||||
|
||||
# Should appear on start date
|
||||
{:ok, day1_bookings} =
|
||||
BookingSystem.list_accepted_space_bookings_by_date(space.id, start_date)
|
||||
|
||||
assert length(day1_bookings) == 1
|
||||
|
||||
# Should appear on middle date
|
||||
middle_date = Date.add(start_date, 1)
|
||||
|
||||
{:ok, day2_bookings} =
|
||||
BookingSystem.list_accepted_space_bookings_by_date(space.id, middle_date)
|
||||
|
||||
assert length(day2_bookings) == 1
|
||||
|
||||
# Should appear on end date
|
||||
{:ok, day4_bookings} =
|
||||
BookingSystem.list_accepted_space_bookings_by_date(space.id, end_date)
|
||||
|
||||
assert length(day4_bookings) == 1
|
||||
|
||||
# Should not appear on day after end date
|
||||
day_after = Date.add(end_date, 1)
|
||||
|
||||
{:ok, day_after_bookings} =
|
||||
BookingSystem.list_accepted_space_bookings_by_date(space.id, day_after)
|
||||
|
||||
assert length(day_after_bookings) == 0
|
||||
end
|
||||
|
||||
test "multi-day booking correctly counts toward availability on all days", %{space: space} do
|
||||
start_date = Date.add(Date.utc_today(), 1)
|
||||
end_date = Date.add(Date.utc_today(), 3)
|
||||
|
||||
start_datetime = DateTime.new!(start_date, ~T[09:00:00], "Etc/UTC")
|
||||
end_datetime = DateTime.new!(end_date, ~T[18:00:00], "Etc/UTC")
|
||||
|
||||
{:ok, _booking} =
|
||||
BookingSystem.create_walk_in(
|
||||
space.id,
|
||||
start_datetime,
|
||||
end_datetime,
|
||||
"Test User",
|
||||
"test@example.com",
|
||||
nil,
|
||||
nil
|
||||
)
|
||||
|
||||
# Check availability on start date
|
||||
{:ok, availability_day1} =
|
||||
BookingSystem.check_availability(space.id, start_date, ~T[10:00:00], ~T[16:00:00])
|
||||
|
||||
# Should show reduced availability due to the multi-day booking
|
||||
assert availability_day1 in [:available, :over_public_capacity]
|
||||
|
||||
# Check availability on middle date
|
||||
middle_date = Date.add(start_date, 1)
|
||||
|
||||
{:ok, availability_day2} =
|
||||
BookingSystem.check_availability(space.id, middle_date, ~T[10:00:00], ~T[16:00:00])
|
||||
|
||||
assert availability_day2 in [:available, :over_public_capacity]
|
||||
|
||||
# Check availability on end date
|
||||
{:ok, availability_day3} =
|
||||
BookingSystem.check_availability(space.id, end_date, ~T[10:00:00], ~T[16:00:00])
|
||||
|
||||
assert availability_day3 in [:available, :over_public_capacity]
|
||||
end
|
||||
|
||||
test "multiple overlapping multi-day bookings correctly fill capacity", %{space: space} do
|
||||
start_date = Date.add(Date.utc_today(), 1)
|
||||
end_date = Date.add(Date.utc_today(), 3)
|
||||
|
||||
# Create 5 multi-day bookings (public capacity)
|
||||
for i <- 1..5 do
|
||||
start_datetime = DateTime.new!(start_date, ~T[09:00:00], "Etc/UTC")
|
||||
end_datetime = DateTime.new!(end_date, ~T[18:00:00], "Etc/UTC")
|
||||
|
||||
{:ok, _booking} =
|
||||
BookingSystem.create_walk_in(
|
||||
space.id,
|
||||
start_datetime,
|
||||
end_datetime,
|
||||
"User #{i}",
|
||||
"user#{i}@example.com",
|
||||
nil,
|
||||
nil
|
||||
)
|
||||
end
|
||||
|
||||
# Check that public capacity is reached on middle day
|
||||
middle_date = Date.add(start_date, 1)
|
||||
|
||||
{:ok, availability} =
|
||||
BookingSystem.check_availability(space.id, middle_date, ~T[10:00:00], ~T[16:00:00])
|
||||
|
||||
assert availability == :over_capacity
|
||||
end
|
||||
|
||||
test "can have both single-day and multi-day bookings on the same day", %{space: space} do
|
||||
date = Date.add(Date.utc_today(), 1)
|
||||
|
||||
# Create a multi-day booking
|
||||
multi_start = DateTime.new!(date, ~T[09:00:00], "Etc/UTC")
|
||||
multi_end = DateTime.new!(Date.add(date, 2), ~T[18:00:00], "Etc/UTC")
|
||||
|
||||
{:ok, _multi_booking} =
|
||||
BookingSystem.create_walk_in(
|
||||
space.id,
|
||||
multi_start,
|
||||
multi_end,
|
||||
"Multi Day User",
|
||||
"multi@example.com",
|
||||
nil,
|
||||
nil
|
||||
)
|
||||
|
||||
# Create a single-day booking on the same date
|
||||
single_start = DateTime.new!(date, ~T[10:00:00], "Etc/UTC")
|
||||
single_end = DateTime.new!(date, ~T[16:00:00], "Etc/UTC")
|
||||
|
||||
{:ok, _single_booking} =
|
||||
BookingSystem.create_walk_in(
|
||||
space.id,
|
||||
single_start,
|
||||
single_end,
|
||||
"Single Day User",
|
||||
"single@example.com",
|
||||
nil,
|
||||
nil
|
||||
)
|
||||
|
||||
# Both should appear in the query for that date
|
||||
{:ok, bookings} = BookingSystem.list_accepted_space_bookings_by_date(space.id, date)
|
||||
assert length(bookings) == 2
|
||||
|
||||
customer_names = Enum.map(bookings, & &1.customer_name)
|
||||
assert "Multi Day User" in customer_names
|
||||
assert "Single Day User" in customer_names
|
||||
end
|
||||
|
||||
test "slot booking counts correctly include multi-day bookings", %{space: space} do
|
||||
date = Date.add(Date.utc_today(), 1)
|
||||
|
||||
# Create a multi-day booking that includes this date
|
||||
multi_start = DateTime.new!(Date.add(date, -1), ~T[09:00:00], "Etc/UTC")
|
||||
multi_end = DateTime.new!(Date.add(date, 1), ~T[18:00:00], "Etc/UTC")
|
||||
|
||||
{:ok, _multi_booking} =
|
||||
BookingSystem.create_walk_in(
|
||||
space.id,
|
||||
multi_start,
|
||||
multi_end,
|
||||
"Multi Day User",
|
||||
"multi@example.com",
|
||||
nil,
|
||||
nil
|
||||
)
|
||||
|
||||
# Create a single-day booking on the same date
|
||||
single_start = DateTime.new!(date, ~T[10:00:00], "Etc/UTC")
|
||||
single_end = DateTime.new!(date, ~T[16:00:00], "Etc/UTC")
|
||||
|
||||
{:ok, _single_booking} =
|
||||
BookingSystem.create_walk_in(
|
||||
space.id,
|
||||
single_start,
|
||||
single_end,
|
||||
"Single Day User",
|
||||
"single@example.com",
|
||||
nil,
|
||||
nil
|
||||
)
|
||||
|
||||
# Get slot counts for a time range on that date
|
||||
{:ok, counts} =
|
||||
BookingSystem.get_slot_booking_counts(space.id, date, ~T[11:00:00], ~T[15:00:00])
|
||||
|
||||
# Should count both bookings
|
||||
assert counts.approved == 2
|
||||
assert counts.pending == 0
|
||||
end
|
||||
|
||||
test "multi-day booking with different start and end times", %{space: space} do
|
||||
start_date = Date.add(Date.utc_today(), 1)
|
||||
end_date = Date.add(Date.utc_today(), 5)
|
||||
|
||||
# Booking starts at 2 PM on day 1 and ends at 11 AM on day 5
|
||||
start_datetime = DateTime.new!(start_date, ~T[14:00:00], "Etc/UTC")
|
||||
end_datetime = DateTime.new!(end_date, ~T[11:00:00], "Etc/UTC")
|
||||
|
||||
{:ok, booking} =
|
||||
BookingSystem.create_walk_in(
|
||||
space.id,
|
||||
start_datetime,
|
||||
end_datetime,
|
||||
"Extended Stay User",
|
||||
"extended@example.com",
|
||||
"+1234567890",
|
||||
"Long term booking"
|
||||
)
|
||||
|
||||
assert booking.start_datetime == start_datetime
|
||||
assert booking.end_datetime == end_datetime
|
||||
assert booking.customer_phone == "+1234567890"
|
||||
assert booking.customer_comment == "Long term booking"
|
||||
|
||||
# Verify it appears on all days
|
||||
for day_offset <- 0..4 do
|
||||
check_date = Date.add(start_date, day_offset)
|
||||
{:ok, bookings} = BookingSystem.list_accepted_space_bookings_by_date(space.id, check_date)
|
||||
assert length(bookings) == 1
|
||||
assert hd(bookings).customer_name == "Extended Stay User"
|
||||
end
|
||||
end
|
||||
|
||||
test "multi-day booking does not appear on days outside its range", %{space: space} do
|
||||
start_date = Date.add(Date.utc_today(), 5)
|
||||
end_date = Date.add(Date.utc_today(), 7)
|
||||
|
||||
start_datetime = DateTime.new!(start_date, ~T[09:00:00], "Etc/UTC")
|
||||
end_datetime = DateTime.new!(end_date, ~T[18:00:00], "Etc/UTC")
|
||||
|
||||
{:ok, _booking} =
|
||||
BookingSystem.create_walk_in(
|
||||
space.id,
|
||||
start_datetime,
|
||||
end_datetime,
|
||||
"Range Test User",
|
||||
"range@example.com",
|
||||
nil,
|
||||
nil
|
||||
)
|
||||
|
||||
# Should not appear on day before start
|
||||
day_before = Date.add(start_date, -1)
|
||||
|
||||
{:ok, bookings_before} =
|
||||
BookingSystem.list_accepted_space_bookings_by_date(space.id, day_before)
|
||||
|
||||
assert length(bookings_before) == 0
|
||||
|
||||
# Should appear on start date
|
||||
{:ok, bookings_start} =
|
||||
BookingSystem.list_accepted_space_bookings_by_date(space.id, start_date)
|
||||
|
||||
assert length(bookings_start) == 1
|
||||
|
||||
# Should appear on end date
|
||||
{:ok, bookings_end} = BookingSystem.list_accepted_space_bookings_by_date(space.id, end_date)
|
||||
assert length(bookings_end) == 1
|
||||
|
||||
# Should not appear on day after end
|
||||
day_after = Date.add(end_date, 1)
|
||||
|
||||
{:ok, bookings_after} =
|
||||
BookingSystem.list_accepted_space_bookings_by_date(space.id, day_after)
|
||||
|
||||
assert length(bookings_after) == 0
|
||||
end
|
||||
|
||||
test "very long multi-day booking (30 days)", %{space: space} do
|
||||
start_date = Date.add(Date.utc_today(), 1)
|
||||
end_date = Date.add(Date.utc_today(), 30)
|
||||
|
||||
start_datetime = DateTime.new!(start_date, ~T[09:00:00], "Etc/UTC")
|
||||
end_datetime = DateTime.new!(end_date, ~T[18:00:00], "Etc/UTC")
|
||||
|
||||
{:ok, booking} =
|
||||
BookingSystem.create_walk_in(
|
||||
space.id,
|
||||
start_datetime,
|
||||
end_datetime,
|
||||
"Long Term User",
|
||||
"longterm@example.com",
|
||||
nil,
|
||||
"Monthly booking"
|
||||
)
|
||||
|
||||
assert booking.start_datetime == start_datetime
|
||||
assert booking.end_datetime == end_datetime
|
||||
|
||||
# Spot check a few days
|
||||
for day_offset <- [0, 10, 20, 29] do
|
||||
check_date = Date.add(start_date, day_offset)
|
||||
{:ok, bookings} = BookingSystem.list_accepted_space_bookings_by_date(space.id, check_date)
|
||||
assert length(bookings) == 1
|
||||
end
|
||||
|
||||
# Verify it doesn't appear the day after
|
||||
day_after = Date.add(end_date, 1)
|
||||
|
||||
{:ok, bookings_after} =
|
||||
BookingSystem.list_accepted_space_bookings_by_date(space.id, day_after)
|
||||
|
||||
assert length(bookings_after) == 0
|
||||
end
|
||||
end
|
||||
|
||||
describe "datetime range overlaps" do
|
||||
test "detects overlap when new booking starts during existing booking", %{space: space} do
|
||||
# Existing booking: Day 1-3
|
||||
day1 = Date.add(Date.utc_today(), 1)
|
||||
day2 = Date.add(Date.utc_today(), 2)
|
||||
day3 = Date.add(Date.utc_today(), 3)
|
||||
|
||||
existing_start = DateTime.new!(day1, ~T[09:00:00], "Etc/UTC")
|
||||
existing_end = DateTime.new!(day3, ~T[18:00:00], "Etc/UTC")
|
||||
|
||||
{:ok, _existing} =
|
||||
BookingSystem.create_walk_in(
|
||||
space.id,
|
||||
existing_start,
|
||||
existing_end,
|
||||
"Existing User",
|
||||
"existing@example.com",
|
||||
nil,
|
||||
nil
|
||||
)
|
||||
|
||||
# Check overlap on day 2
|
||||
{:ok, counts} =
|
||||
BookingSystem.get_slot_booking_counts(space.id, day2, ~T[10:00:00], ~T[16:00:00])
|
||||
|
||||
assert counts.approved == 1
|
||||
end
|
||||
|
||||
test "detects overlap when new booking ends during existing booking", %{space: space} do
|
||||
# Existing booking: Day 3-5
|
||||
day3 = Date.add(Date.utc_today(), 3)
|
||||
day4 = Date.add(Date.utc_today(), 4)
|
||||
day5 = Date.add(Date.utc_today(), 5)
|
||||
|
||||
existing_start = DateTime.new!(day3, ~T[09:00:00], "Etc/UTC")
|
||||
existing_end = DateTime.new!(day5, ~T[18:00:00], "Etc/UTC")
|
||||
|
||||
{:ok, _existing} =
|
||||
BookingSystem.create_walk_in(
|
||||
space.id,
|
||||
existing_start,
|
||||
existing_end,
|
||||
"Existing User",
|
||||
"existing@example.com",
|
||||
nil,
|
||||
nil
|
||||
)
|
||||
|
||||
# Check availability on day 4
|
||||
{:ok, counts} =
|
||||
BookingSystem.get_slot_booking_counts(space.id, day4, ~T[10:00:00], ~T[16:00:00])
|
||||
|
||||
assert counts.approved == 1
|
||||
end
|
||||
|
||||
test "detects overlap when new booking completely contains existing booking", %{space: space} do
|
||||
# Existing booking: Day 3-5
|
||||
day3 = Date.add(Date.utc_today(), 3)
|
||||
day4 = Date.add(Date.utc_today(), 4)
|
||||
day5 = Date.add(Date.utc_today(), 5)
|
||||
|
||||
existing_start = DateTime.new!(day3, ~T[09:00:00], "Etc/UTC")
|
||||
existing_end = DateTime.new!(day5, ~T[18:00:00], "Etc/UTC")
|
||||
|
||||
{:ok, _existing} =
|
||||
BookingSystem.create_walk_in(
|
||||
space.id,
|
||||
existing_start,
|
||||
existing_end,
|
||||
"Existing User",
|
||||
"existing@example.com",
|
||||
nil,
|
||||
nil
|
||||
)
|
||||
|
||||
# Check if overlaps on day 4 (middle day)
|
||||
{:ok, counts} =
|
||||
BookingSystem.get_slot_booking_counts(space.id, day4, ~T[10:00:00], ~T[16:00:00])
|
||||
|
||||
assert counts.approved == 1
|
||||
end
|
||||
|
||||
test "detects overlap when new booking is contained within existing booking", %{space: space} do
|
||||
# Existing booking: Day 1-10
|
||||
day1 = Date.add(Date.utc_today(), 1)
|
||||
day5 = Date.add(Date.utc_today(), 5)
|
||||
day10 = Date.add(Date.utc_today(), 10)
|
||||
|
||||
existing_start = DateTime.new!(day1, ~T[09:00:00], "Etc/UTC")
|
||||
existing_end = DateTime.new!(day10, ~T[18:00:00], "Etc/UTC")
|
||||
|
||||
{:ok, _existing} =
|
||||
BookingSystem.create_walk_in(
|
||||
space.id,
|
||||
existing_start,
|
||||
existing_end,
|
||||
"Existing User",
|
||||
"existing@example.com",
|
||||
nil,
|
||||
nil
|
||||
)
|
||||
|
||||
# Check availability on day 5 (middle day within long booking)
|
||||
{:ok, counts} =
|
||||
BookingSystem.get_slot_booking_counts(space.id, day5, ~T[10:00:00], ~T[16:00:00])
|
||||
|
||||
assert counts.approved == 1
|
||||
end
|
||||
|
||||
test "no overlap when bookings are on consecutive days with no time overlap", %{space: space} do
|
||||
# First booking: Day 1-2, ending at 12 PM on day 2
|
||||
day1 = Date.add(Date.utc_today(), 1)
|
||||
day2 = Date.add(Date.utc_today(), 2)
|
||||
|
||||
first_start = DateTime.new!(day1, ~T[09:00:00], "Etc/UTC")
|
||||
first_end = DateTime.new!(day2, ~T[12:00:00], "Etc/UTC")
|
||||
|
||||
{:ok, _first} =
|
||||
BookingSystem.create_walk_in(
|
||||
space.id,
|
||||
first_start,
|
||||
first_end,
|
||||
"First User",
|
||||
"first@example.com",
|
||||
nil,
|
||||
nil
|
||||
)
|
||||
|
||||
# Check availability on day 2 afternoon (after first booking ends)
|
||||
{:ok, counts} =
|
||||
BookingSystem.get_slot_booking_counts(space.id, day2, ~T[13:00:00], ~T[18:00:00])
|
||||
|
||||
assert counts.approved == 0
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -85,287 +85,4 @@ defmodule SpazioSolazzo.BookingSystem.SpaceTest do
|
|||
assert space1.id != space2.id
|
||||
end
|
||||
end
|
||||
|
||||
describe "check_availability/4" do
|
||||
setup do
|
||||
{:ok, space} =
|
||||
BookingSystem.create_space(
|
||||
"Availability Test Space",
|
||||
"availability-test",
|
||||
"Test description",
|
||||
2
|
||||
)
|
||||
|
||||
%{space: space}
|
||||
end
|
||||
|
||||
test "returns :available when no bookings exist", %{space: space} do
|
||||
date = Date.utc_today()
|
||||
start_time = ~T[09:00:00]
|
||||
end_time = ~T[10:00:00]
|
||||
|
||||
assert {:ok, :available} =
|
||||
BookingSystem.check_availability(space.id, date, start_time, end_time)
|
||||
end
|
||||
|
||||
test "returns :available when under capacity", %{space: space} do
|
||||
date = Date.add(Date.utc_today(), 1)
|
||||
start_time = ~T[09:00:00]
|
||||
end_time = ~T[10:00:00]
|
||||
|
||||
BookingSystem.create_walk_in(
|
||||
space.id,
|
||||
DateTime.new!(date, start_time, "Etc/UTC"),
|
||||
DateTime.new!(date, end_time, "Etc/UTC"),
|
||||
"Customer 1",
|
||||
"customer1@example.com",
|
||||
nil,
|
||||
nil
|
||||
)
|
||||
|
||||
assert {:ok, :available} =
|
||||
BookingSystem.check_availability(space.id, date, start_time, end_time)
|
||||
end
|
||||
|
||||
test "returns :over_capacity when at or over capacity", %{space: space} do
|
||||
date = Date.add(Date.utc_today(), 1)
|
||||
start_time = ~T[09:00:00]
|
||||
end_time = ~T[10:00:00]
|
||||
|
||||
BookingSystem.create_walk_in(
|
||||
space.id,
|
||||
DateTime.new!(date, start_time, "Etc/UTC"),
|
||||
DateTime.new!(date, end_time, "Etc/UTC"),
|
||||
"Customer 1",
|
||||
"customer1@example.com",
|
||||
nil,
|
||||
nil
|
||||
)
|
||||
|
||||
BookingSystem.create_walk_in(
|
||||
space.id,
|
||||
DateTime.new!(date, start_time, "Etc/UTC"),
|
||||
DateTime.new!(date, end_time, "Etc/UTC"),
|
||||
"Customer 2",
|
||||
"customer2@example.com",
|
||||
nil,
|
||||
nil
|
||||
)
|
||||
|
||||
assert {:ok, :over_capacity} =
|
||||
BookingSystem.check_availability(space.id, date, start_time, end_time)
|
||||
end
|
||||
|
||||
test "only counts overlapping bookings", %{space: space} do
|
||||
date = Date.utc_today()
|
||||
start_time = ~T[09:00:00]
|
||||
end_time = ~T[10:00:00]
|
||||
|
||||
BookingSystem.create_walk_in(
|
||||
space.id,
|
||||
DateTime.new!(date, ~T[08:00:00], "Etc/UTC"),
|
||||
DateTime.new!(date, ~T[09:00:00], "Etc/UTC"),
|
||||
"Customer 1",
|
||||
"customer1@example.com",
|
||||
nil,
|
||||
nil
|
||||
)
|
||||
|
||||
BookingSystem.create_walk_in(
|
||||
space.id,
|
||||
DateTime.new!(date, ~T[10:00:00], "Etc/UTC"),
|
||||
DateTime.new!(date, ~T[11:00:00], "Etc/UTC"),
|
||||
"Customer 2",
|
||||
"customer2@example.com",
|
||||
nil,
|
||||
nil
|
||||
)
|
||||
|
||||
assert {:ok, :available} =
|
||||
BookingSystem.check_availability(space.id, date, start_time, end_time)
|
||||
end
|
||||
|
||||
test "counts partial overlaps", %{space: space} do
|
||||
date = Date.add(Date.utc_today(), 1)
|
||||
start_time = ~T[09:00:00]
|
||||
end_time = ~T[10:00:00]
|
||||
|
||||
BookingSystem.create_walk_in(
|
||||
space.id,
|
||||
DateTime.new!(date, ~T[08:30:00], "Etc/UTC"),
|
||||
DateTime.new!(date, ~T[09:30:00], "Etc/UTC"),
|
||||
"Customer 1",
|
||||
"customer1@example.com",
|
||||
nil,
|
||||
nil
|
||||
)
|
||||
|
||||
BookingSystem.create_walk_in(
|
||||
space.id,
|
||||
DateTime.new!(date, ~T[09:30:00], "Etc/UTC"),
|
||||
DateTime.new!(date, ~T[10:30:00], "Etc/UTC"),
|
||||
"Customer 2",
|
||||
"customer2@example.com",
|
||||
nil,
|
||||
nil
|
||||
)
|
||||
|
||||
assert {:ok, :over_capacity} =
|
||||
BookingSystem.check_availability(space.id, date, start_time, end_time)
|
||||
end
|
||||
|
||||
test "does not count pending bookings", %{space: space} do
|
||||
date = Date.add(Date.utc_today(), 1)
|
||||
start_time = ~T[09:00:00]
|
||||
end_time = ~T[10:00:00]
|
||||
|
||||
BookingSystem.create_booking(
|
||||
space.id,
|
||||
nil,
|
||||
date,
|
||||
start_time,
|
||||
end_time,
|
||||
"Customer 1",
|
||||
"customer1@example.com",
|
||||
nil,
|
||||
nil
|
||||
)
|
||||
|
||||
BookingSystem.create_booking(
|
||||
space.id,
|
||||
nil,
|
||||
date,
|
||||
start_time,
|
||||
end_time,
|
||||
"Customer 2",
|
||||
"customer2@example.com",
|
||||
nil,
|
||||
nil
|
||||
)
|
||||
|
||||
assert {:ok, :available} =
|
||||
BookingSystem.check_availability(space.id, date, start_time, end_time)
|
||||
end
|
||||
|
||||
test "does not count cancelled bookings", %{space: space} do
|
||||
date = Date.add(Date.utc_today(), 1)
|
||||
start_time = ~T[09:00:00]
|
||||
end_time = ~T[10:00:00]
|
||||
|
||||
{:ok, booking1} =
|
||||
BookingSystem.create_walk_in(
|
||||
space.id,
|
||||
DateTime.new!(date, start_time, "Etc/UTC"),
|
||||
DateTime.new!(date, end_time, "Etc/UTC"),
|
||||
"Customer 1",
|
||||
"customer1@example.com",
|
||||
nil,
|
||||
nil
|
||||
)
|
||||
|
||||
{:ok, _} =
|
||||
BookingSystem.create_walk_in(
|
||||
space.id,
|
||||
DateTime.new!(date, start_time, "Etc/UTC"),
|
||||
DateTime.new!(date, end_time, "Etc/UTC"),
|
||||
"Customer 2",
|
||||
"customer2@example.com",
|
||||
nil,
|
||||
nil
|
||||
)
|
||||
|
||||
BookingSystem.cancel_booking(booking1, "User requested cancellation")
|
||||
|
||||
assert {:ok, :available} =
|
||||
BookingSystem.check_availability(space.id, date, start_time, end_time)
|
||||
end
|
||||
|
||||
test "does not count rejected bookings", %{space: space} do
|
||||
date = Date.add(Date.utc_today(), 1)
|
||||
start_time = ~T[09:00:00]
|
||||
end_time = ~T[10:00:00]
|
||||
|
||||
{:ok, booking1} =
|
||||
BookingSystem.create_booking(
|
||||
space.id,
|
||||
nil,
|
||||
date,
|
||||
start_time,
|
||||
end_time,
|
||||
"Customer 1",
|
||||
"customer1@example.com",
|
||||
nil,
|
||||
nil
|
||||
)
|
||||
|
||||
{:ok, _} =
|
||||
BookingSystem.create_booking(
|
||||
space.id,
|
||||
nil,
|
||||
date,
|
||||
start_time,
|
||||
end_time,
|
||||
"Customer 2",
|
||||
"customer2@example.com",
|
||||
nil,
|
||||
nil
|
||||
)
|
||||
|
||||
BookingSystem.reject_booking(booking1, "Space not available")
|
||||
|
||||
assert {:ok, :available} =
|
||||
BookingSystem.check_availability(space.id, date, start_time, end_time)
|
||||
end
|
||||
|
||||
test "filters by date correctly", %{space: space} do
|
||||
date1 = Date.utc_today()
|
||||
date2 = Date.add(Date.utc_today(), 1)
|
||||
start_time = ~T[09:00:00]
|
||||
end_time = ~T[10:00:00]
|
||||
|
||||
for i <- 1..3 do
|
||||
BookingSystem.create_walk_in(
|
||||
space.id,
|
||||
DateTime.new!(date1, start_time, "Etc/UTC"),
|
||||
DateTime.new!(date1, end_time, "Etc/UTC"),
|
||||
"Customer #{i}",
|
||||
"customer#{i}@example.com",
|
||||
nil,
|
||||
nil
|
||||
)
|
||||
end
|
||||
|
||||
assert {:ok, :available} =
|
||||
BookingSystem.check_availability(space.id, date2, start_time, end_time)
|
||||
end
|
||||
|
||||
test "filters by space correctly", %{space: space} do
|
||||
{:ok, other_space} =
|
||||
BookingSystem.create_space(
|
||||
"Other Space",
|
||||
"other-space",
|
||||
"Another test space",
|
||||
2
|
||||
)
|
||||
|
||||
date = Date.add(Date.utc_today(), 1)
|
||||
start_time = ~T[09:00:00]
|
||||
end_time = ~T[10:00:00]
|
||||
|
||||
for i <- 1..3 do
|
||||
BookingSystem.create_walk_in(
|
||||
other_space.id,
|
||||
DateTime.new!(date, start_time, "Etc/UTC"),
|
||||
DateTime.new!(date, end_time, "Etc/UTC"),
|
||||
"Customer #{i}",
|
||||
"customer#{i}@example.com",
|
||||
nil,
|
||||
nil
|
||||
)
|
||||
end
|
||||
|
||||
assert {:ok, :available} =
|
||||
BookingSystem.check_availability(space.id, date, start_time, end_time)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -251,7 +251,11 @@ defmodule SpazioSolazzoWeb.BookingLive.SpaceBookingTest do
|
|||
assert html =~ "High Demand - Join Waitlist"
|
||||
end
|
||||
|
||||
test "shows slots over capacity with high demand warning", %{conn: conn, space: space, today: today} do
|
||||
test "shows slots over capacity with high demand warning", %{
|
||||
conn: conn,
|
||||
space: space,
|
||||
today: today
|
||||
} do
|
||||
for i <- 1..3 do
|
||||
{:ok, booking} =
|
||||
request_booking(
|
||||
|
|
|
|||
Loading…
Reference in a new issue