mirror of
https://codeberg.org/JasterV/spazio-solazzo.git
synced 2026-04-26 18:20:03 +00:00
do not let users request twice the same slot
This commit is contained in:
parent
b48af6d3e6
commit
5afff793dc
16 changed files with 956 additions and 210 deletions
|
|
@ -95,20 +95,6 @@ defmodule SpazioSolazzo.BookingSystem.Space do
|
|||
end
|
||||
end
|
||||
|
||||
attributes do
|
||||
uuid_primary_key :id
|
||||
attribute :name, :string, allow_nil?: false, public?: true
|
||||
attribute :description, :string, allow_nil?: false, public?: true
|
||||
attribute :slug, :string, allow_nil?: false, public?: true
|
||||
attribute :public_capacity, :integer, allow_nil?: false, public?: true
|
||||
attribute :real_capacity, :integer, allow_nil?: false, public?: true
|
||||
end
|
||||
|
||||
identities do
|
||||
identity :unique_name, [:name]
|
||||
identity :unique_slug, [:slug]
|
||||
end
|
||||
|
||||
policies do
|
||||
policy action(:check_availability) do
|
||||
authorize_if always()
|
||||
|
|
@ -122,4 +108,18 @@ defmodule SpazioSolazzo.BookingSystem.Space do
|
|||
authorize_if always()
|
||||
end
|
||||
end
|
||||
|
||||
attributes do
|
||||
uuid_primary_key :id
|
||||
attribute :name, :string, allow_nil?: false, public?: true
|
||||
attribute :description, :string, allow_nil?: false, public?: true
|
||||
attribute :slug, :string, allow_nil?: false, public?: true
|
||||
attribute :public_capacity, :integer, allow_nil?: false, public?: true
|
||||
attribute :real_capacity, :integer, allow_nil?: false, public?: true
|
||||
end
|
||||
|
||||
identities do
|
||||
identity :unique_name, [:name]
|
||||
identity :unique_slug, [:slug]
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -223,8 +223,12 @@ defmodule SpazioSolazzoWeb.Admin.AdminCalendarComponent do
|
|||
end
|
||||
|
||||
defp day_in_range?(_date, nil, nil, nil), do: false
|
||||
defp day_in_range?(date, selected, nil, nil) when not is_nil(selected), do: Date.compare(date, selected) == :eq
|
||||
defp day_in_range?(date, nil, start_date, nil) when not is_nil(start_date), do: Date.compare(date, start_date) == :eq
|
||||
|
||||
defp day_in_range?(date, selected, nil, nil) when not is_nil(selected),
|
||||
do: Date.compare(date, selected) == :eq
|
||||
|
||||
defp day_in_range?(date, nil, start_date, nil) when not is_nil(start_date),
|
||||
do: Date.compare(date, start_date) == :eq
|
||||
|
||||
defp day_in_range?(date, nil, start_date, end_date)
|
||||
when not is_nil(start_date) and not is_nil(end_date) do
|
||||
|
|
@ -254,28 +258,49 @@ defmodule SpazioSolazzoWeb.Admin.AdminCalendarComponent do
|
|||
[base, "text-slate-400 dark:text-slate-600 cursor-not-allowed opacity-50"]
|
||||
|
||||
capacity == :over_real_capacity ->
|
||||
[base, "bg-red-50 dark:bg-red-900/20 text-slate-400 dark:text-slate-500 border border-red-300 dark:border-red-800/30 cursor-not-allowed"]
|
||||
[
|
||||
base,
|
||||
"bg-red-50 dark:bg-red-900/20 text-slate-400 dark:text-slate-500 border border-red-300 dark:border-red-800/30 cursor-not-allowed"
|
||||
]
|
||||
|
||||
in_range && assigns.multi_day_mode && assigns.end_date != nil ->
|
||||
cond do
|
||||
is_start ->
|
||||
[base, "rounded-l-lg bg-primary text-white shadow-lg shadow-primary/30 relative z-10 hover:scale-105"]
|
||||
[
|
||||
base,
|
||||
"rounded-l-lg bg-primary text-white shadow-lg shadow-primary/30 relative z-10 hover:scale-105"
|
||||
]
|
||||
|
||||
is_end ->
|
||||
[base, "rounded-r-lg bg-primary text-white shadow-lg shadow-primary/30 relative z-10 hover:scale-105"]
|
||||
[
|
||||
base,
|
||||
"rounded-r-lg bg-primary text-white shadow-lg shadow-primary/30 relative z-10 hover:scale-105"
|
||||
]
|
||||
|
||||
true ->
|
||||
[base, "bg-primary/20 dark:bg-primary/30 text-slate-900 dark:text-white border-y border-primary/20 dark:border-primary/50"]
|
||||
[
|
||||
base,
|
||||
"bg-primary/20 dark:bg-primary/30 text-slate-900 dark:text-white border-y border-primary/20 dark:border-primary/50"
|
||||
]
|
||||
end
|
||||
|
||||
in_range ->
|
||||
[base, "rounded-lg bg-primary text-white shadow-lg shadow-primary/30 relative z-10 hover:scale-105"]
|
||||
[
|
||||
base,
|
||||
"rounded-lg bg-primary text-white shadow-lg shadow-primary/30 relative z-10 hover:scale-105"
|
||||
]
|
||||
|
||||
capacity == :over_public_capacity ->
|
||||
[base, "rounded-lg bg-orange-100 dark:bg-orange-900/20 hover:bg-orange-200 dark:hover:bg-orange-900/40 text-slate-700 dark:text-slate-200 border border-transparent hover:border-orange-500 dark:hover:border-orange-600"]
|
||||
[
|
||||
base,
|
||||
"rounded-lg bg-orange-100 dark:bg-orange-900/20 hover:bg-orange-200 dark:hover:bg-orange-900/40 text-slate-700 dark:text-slate-200 border border-transparent hover:border-orange-500 dark:hover:border-orange-600"
|
||||
]
|
||||
|
||||
true ->
|
||||
[base, "rounded-lg bg-green-100 dark:bg-green-900/20 hover:bg-green-200 dark:hover:bg-green-900/40 text-slate-700 dark:text-slate-200 border border-transparent hover:border-green-500 dark:hover:border-green-600"]
|
||||
[
|
||||
base,
|
||||
"rounded-lg bg-green-100 dark:bg-green-900/20 hover:bg-green-200 dark:hover:bg-green-900/40 text-slate-700 dark:text-slate-200 border border-transparent hover:border-green-500 dark:hover:border-green-600"
|
||||
]
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -12,7 +12,10 @@
|
|||
class="size-4 rounded border-slate-300 dark:border-slate-600 text-primary focus:ring-primary dark:bg-slate-700 pointer-events-none"
|
||||
readonly
|
||||
/>
|
||||
<label for={"multi-day-#{@id}"} class="text-sm font-semibold text-slate-700 dark:text-slate-300 select-none flex-1">
|
||||
<label
|
||||
for={"multi-day-#{@id}"}
|
||||
class="text-sm font-semibold text-slate-700 dark:text-slate-300 select-none flex-1"
|
||||
>
|
||||
Enable Multi-Day Selection
|
||||
</label>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -119,6 +119,10 @@ defmodule SpazioSolazzoWeb.Admin.BookingManagementLive do
|
|||
)}
|
||||
end
|
||||
|
||||
def handle_event("stop_propagation", _, socket) do
|
||||
{:noreply, socket}
|
||||
end
|
||||
|
||||
def handle_event("update_rejection_reason", %{"reason" => reason}, socket) do
|
||||
{:noreply, assign(socket, rejection_reason: reason)}
|
||||
end
|
||||
|
|
|
|||
|
|
@ -174,7 +174,9 @@
|
|||
</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)}
|
||||
{SpazioSolazzo.CalendarExt.format_datetime_range_start(
|
||||
booking.start_datetime
|
||||
)}
|
||||
</p>
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap">
|
||||
|
|
@ -324,7 +326,9 @@
|
|||
</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)}
|
||||
{SpazioSolazzo.CalendarExt.format_datetime_range_start(
|
||||
booking.start_datetime
|
||||
)}
|
||||
</p>
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap">
|
||||
|
|
@ -431,21 +435,21 @@
|
|||
<%!-- Reject Modal --%>
|
||||
<%= if @show_reject_modal do %>
|
||||
<div
|
||||
class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center p-4 z-50"
|
||||
class="fixed inset-0 bg-slate-900/20 backdrop-blur-sm flex items-center justify-center p-4 z-50"
|
||||
phx-click="hide_reject_modal"
|
||||
>
|
||||
<div
|
||||
class="bg-white dark:bg-slate-800 rounded-2xl shadow-2xl max-w-md w-full p-8"
|
||||
class="bg-white dark:bg-slate-800 rounded-3xl border border-slate-200 dark:border-slate-700 shadow-xl shadow-slate-200/50 dark:shadow-none max-w-md w-full p-6"
|
||||
phx-click="stop_propagation"
|
||||
>
|
||||
<h3 class="text-2xl font-bold text-slate-900 dark:text-white mb-4">Reject Booking</h3>
|
||||
<h3 class="text-xl font-bold text-slate-900 dark:text-white mb-3">Reject Booking</h3>
|
||||
|
||||
<p class="text-slate-600 dark:text-slate-400 mb-6">
|
||||
Please provide a reason for rejecting this booking request. The customer will receive this reason in their email.
|
||||
<p class="text-slate-600 dark:text-slate-400 mb-4 text-sm">
|
||||
Provide a reason for rejecting this booking. The customer will receive this in their email.
|
||||
</p>
|
||||
|
||||
<form phx-submit="confirm_reject">
|
||||
<div class="mb-6">
|
||||
<div class="mb-4">
|
||||
<label class="block text-sm font-semibold text-slate-900 dark:text-white mb-2">
|
||||
Rejection Reason <span class="text-red-500">*</span>
|
||||
</label>
|
||||
|
|
@ -455,23 +459,23 @@
|
|||
rows="4"
|
||||
required
|
||||
placeholder="e.g., Space under maintenance, Fully booked, etc."
|
||||
class="w-full px-4 py-3 border border-slate-300 dark:border-slate-600 bg-white dark:bg-slate-700 text-slate-900 dark:text-white rounded-xl focus:outline-none focus:ring-2 focus:ring-primary"
|
||||
class="w-full px-3 py-2 border border-slate-300 dark:border-slate-600 bg-white dark:bg-slate-700 text-slate-900 dark:text-white rounded-xl focus:outline-none focus:ring-2 focus:ring-primary"
|
||||
>{@rejection_reason}</textarea>
|
||||
</div>
|
||||
|
||||
<div class="flex gap-3">
|
||||
<div class="flex gap-2">
|
||||
<button
|
||||
type="button"
|
||||
phx-click="hide_reject_modal"
|
||||
class="flex-1 bg-slate-200 dark:bg-slate-700 text-slate-700 dark:text-slate-200 px-6 py-3 rounded-xl font-semibold hover:bg-slate-300 dark:hover:bg-slate-600 transition-colors"
|
||||
class="flex-1 bg-slate-200 dark:bg-slate-700 text-slate-700 dark:text-slate-200 px-4 py-2 rounded-lg font-medium hover:bg-slate-300 dark:hover:bg-slate-600 transition-colors text-sm"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
class="flex-1 bg-red-500 text-white px-6 py-3 rounded-xl font-semibold hover:bg-red-600 transition-colors"
|
||||
class="flex-1 bg-red-500 text-white px-4 py-2 rounded-lg font-medium hover:bg-red-600 transition-colors text-sm"
|
||||
>
|
||||
Confirm Rejection
|
||||
Reject
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
|
|
|||
|
|
@ -117,7 +117,8 @@ defmodule SpazioSolazzoWeb.Admin.WalkInLive do
|
|||
end
|
||||
else
|
||||
_ ->
|
||||
{:noreply, put_flash(socket, :error, "Please fill in all required fields and select a date")}
|
||||
{:noreply,
|
||||
put_flash(socket, :error, "Please fill in all required fields and select a date")}
|
||||
end
|
||||
end
|
||||
|
||||
|
|
@ -163,7 +164,9 @@ defmodule SpazioSolazzoWeb.Admin.WalkInLive do
|
|||
assign(socket, time_slot_warning: "This time slot is currently full.")
|
||||
|
||||
{:ok, :over_public_capacity} ->
|
||||
assign(socket, time_slot_warning: "This time slot has high demand but space is still available.")
|
||||
assign(socket,
|
||||
time_slot_warning: "This time slot has high demand but space is still available."
|
||||
)
|
||||
|
||||
_ ->
|
||||
assign(socket, time_slot_warning: nil)
|
||||
|
|
|
|||
|
|
@ -34,10 +34,12 @@ defmodule SpazioSolazzoWeb.SpaceBookingLive do
|
|||
time_slots: time_slots,
|
||||
current_scope: nil,
|
||||
slot_availability: %{},
|
||||
slot_booking_counts: %{}
|
||||
slot_booking_counts: %{},
|
||||
user_booked_slots: %{}
|
||||
)
|
||||
|> compute_slot_availability()
|
||||
|> compute_slot_booking_counts()}
|
||||
|> compute_slot_booking_counts()
|
||||
|> compute_user_booked_slots()}
|
||||
|
||||
{:error, _error} ->
|
||||
{:ok,
|
||||
|
|
@ -50,11 +52,16 @@ defmodule SpazioSolazzoWeb.SpaceBookingLive do
|
|||
def handle_event("select_slot", %{"time_slot_id" => time_slot_id}, socket) do
|
||||
time_slot = Enum.find(socket.assigns.time_slots, &(&1.id == time_slot_id))
|
||||
|
||||
{:noreply,
|
||||
assign(socket,
|
||||
selected_time_slot: time_slot,
|
||||
show_booking_modal: true
|
||||
)}
|
||||
# Prevent opening modal if user already has a booking for this slot
|
||||
if socket.assigns.user_booked_slots[time_slot_id] do
|
||||
{:noreply, socket}
|
||||
else
|
||||
{:noreply,
|
||||
assign(socket,
|
||||
selected_time_slot: time_slot,
|
||||
show_booking_modal: true
|
||||
)}
|
||||
end
|
||||
end
|
||||
|
||||
def handle_event("cancel_booking", _params, socket) do
|
||||
|
|
@ -90,6 +97,21 @@ defmodule SpazioSolazzoWeb.SpaceBookingLive do
|
|||
show_success_modal: true
|
||||
)}
|
||||
|
||||
{:error, %Ash.Error.Invalid{errors: errors}} ->
|
||||
error_message =
|
||||
errors
|
||||
|> Enum.map(fn
|
||||
%{field: :date, message: msg} -> msg
|
||||
%{message: msg} -> msg
|
||||
_error -> "Invalid booking request"
|
||||
end)
|
||||
|> Enum.join(", ")
|
||||
|
||||
{:noreply,
|
||||
socket
|
||||
|> assign(show_booking_modal: false)
|
||||
|> put_flash(:error, error_message)}
|
||||
|
||||
{:error, _error} ->
|
||||
{:noreply,
|
||||
socket
|
||||
|
|
@ -113,7 +135,8 @@ defmodule SpazioSolazzoWeb.SpaceBookingLive do
|
|||
bookings: bookings
|
||||
)
|
||||
|> compute_slot_availability()
|
||||
|> compute_slot_booking_counts()}
|
||||
|> compute_slot_booking_counts()
|
||||
|> compute_user_booked_slots()}
|
||||
end
|
||||
|
||||
def handle_info(
|
||||
|
|
@ -126,7 +149,8 @@ defmodule SpazioSolazzoWeb.SpaceBookingLive do
|
|||
socket
|
||||
|> assign(bookings: bookings)
|
||||
|> compute_slot_availability()
|
||||
|> compute_slot_booking_counts()}
|
||||
|> compute_slot_booking_counts()
|
||||
|> compute_user_booked_slots()}
|
||||
end
|
||||
|
||||
def handle_info(_msg, socket) do
|
||||
|
|
@ -170,4 +194,38 @@ defmodule SpazioSolazzoWeb.SpaceBookingLive do
|
|||
|
||||
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)
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -46,18 +46,26 @@
|
|||
<% 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}) %>
|
||||
<% 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) %>
|
||||
<%= if availability != :over_real_capacity do %>
|
||||
<button
|
||||
phx-click="select_slot"
|
||||
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(availability == :available,
|
||||
if(user_has_booking,
|
||||
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",
|
||||
"border-slate-200 bg-slate-100 cursor-not-allowed opacity-60 dark:bg-slate-700/50 dark:border-slate-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"
|
||||
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"
|
||||
)
|
||||
)
|
||||
]}
|
||||
>
|
||||
|
|
@ -69,14 +77,20 @@
|
|||
"%H:%M"
|
||||
)}
|
||||
</div>
|
||||
<%= if availability == :available do %>
|
||||
<div class="text-sm text-green-600 dark:text-green-400 font-medium mt-1">
|
||||
Available - Request Booking
|
||||
<%= if user_has_booking do %>
|
||||
<div class="text-sm text-slate-500 dark:text-slate-400 font-medium mt-1">
|
||||
Already Requested
|
||||
</div>
|
||||
<% else %>
|
||||
<div class="text-sm text-yellow-600 dark:text-yellow-400 font-medium mt-1">
|
||||
High Demand - Join Waitlist
|
||||
</div>
|
||||
<%= 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 %>
|
||||
<% end %>
|
||||
<div class="flex gap-3 mt-2 text-xs text-slate-600 dark:text-slate-400">
|
||||
<%= if counts.pending > 0 do %>
|
||||
|
|
@ -94,12 +108,16 @@
|
|||
</div>
|
||||
</div>
|
||||
<.icon
|
||||
name="hero-arrow-right"
|
||||
name={if user_has_booking, do: "hero-check", else: "hero-arrow-right"}
|
||||
class={[
|
||||
"w-5 h-5",
|
||||
if(availability == :available,
|
||||
do: "text-green-500 dark:text-green-400",
|
||||
else: "text-yellow-500 dark:text-yellow-400"
|
||||
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"
|
||||
)
|
||||
)
|
||||
]}
|
||||
/>
|
||||
|
|
@ -141,14 +159,14 @@
|
|||
<%= if @show_success_modal do %>
|
||||
<div
|
||||
id="success-modal"
|
||||
class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center p-4 z-50"
|
||||
class="fixed inset-0 bg-slate-900/20 backdrop-blur-sm flex items-center justify-center p-4 z-50"
|
||||
phx-click="close_success_modal"
|
||||
>
|
||||
<div class="bg-white dark:bg-slate-800 rounded-2xl shadow-2xl max-w-md w-full p-8 text-center">
|
||||
<div class="mb-6">
|
||||
<div class="mx-auto flex items-center justify-center h-16 w-16 rounded-full bg-green-100 dark:bg-green-900/20 mb-4">
|
||||
<div class="bg-white dark:bg-slate-800 rounded-3xl border border-slate-200 dark:border-slate-700 shadow-xl shadow-slate-200/50 dark:shadow-none max-w-md w-full p-6 text-center">
|
||||
<div class="mb-4">
|
||||
<div class="mx-auto flex items-center justify-center h-14 w-14 rounded-full bg-green-100 dark:bg-green-900/20 mb-3">
|
||||
<svg
|
||||
class="h-10 w-10 text-green-600 dark:text-green-400"
|
||||
class="h-8 w-8 text-green-600 dark:text-green-400"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
|
|
@ -161,17 +179,17 @@
|
|||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<h3 class="text-2xl font-bold text-slate-900 dark:text-white mb-2">
|
||||
<h3 class="text-xl font-bold text-slate-900 dark:text-white mb-2">
|
||||
Request Submitted!
|
||||
</h3>
|
||||
<p class="text-slate-600 dark:text-slate-400">
|
||||
Your booking request has been received and is pending approval. You will receive an email confirmation shortly.
|
||||
<p class="text-slate-600 dark:text-slate-400 text-sm">
|
||||
Your booking request is pending approval. You'll receive an email confirmation shortly.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<button
|
||||
phx-click="close_success_modal"
|
||||
class="w-full bg-green-500 text-white px-6 py-3 rounded-xl font-semibold hover:bg-green-600 transition-colors"
|
||||
class="w-full bg-green-500 text-white px-4 py-2 rounded-lg font-medium hover:bg-green-600 transition-colors text-sm"
|
||||
>
|
||||
Close
|
||||
</button>
|
||||
|
|
|
|||
|
|
@ -21,9 +21,10 @@ defmodule SpazioSolazzo.BookingSystem.Booking.RequestCreatedEmailWorkerTest do
|
|||
|
||||
# Verify customer email was sent
|
||||
emails = Memory.all()
|
||||
|
||||
assert Enum.any?(emails, fn email ->
|
||||
email.to == [{"John Doe", "john@example.com"}]
|
||||
end)
|
||||
email.to == [{"John Doe", "john@example.com"}]
|
||||
end)
|
||||
end
|
||||
|
||||
test "sends notification email to admin" do
|
||||
|
|
@ -44,9 +45,10 @@ defmodule SpazioSolazzo.BookingSystem.Booking.RequestCreatedEmailWorkerTest do
|
|||
|
||||
# Verify admin email was sent
|
||||
emails = Memory.all()
|
||||
|
||||
assert Enum.any?(emails, fn email ->
|
||||
email.to == [{"", admin_email}]
|
||||
end)
|
||||
email.to == [{"", admin_email}]
|
||||
end)
|
||||
end
|
||||
|
||||
test "sends both customer and admin emails in single job execution" do
|
||||
|
|
@ -89,9 +91,11 @@ defmodule SpazioSolazzo.BookingSystem.Booking.RequestCreatedEmailWorkerTest do
|
|||
assert :ok = perform_job(RequestCreatedEmailWorker, job_args)
|
||||
|
||||
emails = Memory.all()
|
||||
customer_email = Enum.find(emails, fn email ->
|
||||
email.to == [{"Test User", "test@example.com"}]
|
||||
end)
|
||||
|
||||
customer_email =
|
||||
Enum.find(emails, fn email ->
|
||||
email.to == [{"Test User", "test@example.com"}]
|
||||
end)
|
||||
|
||||
assert customer_email != nil
|
||||
assert String.contains?(customer_email.html_body, "Music Room")
|
||||
|
|
@ -115,9 +119,11 @@ defmodule SpazioSolazzo.BookingSystem.Booking.RequestCreatedEmailWorkerTest do
|
|||
assert :ok = perform_job(RequestCreatedEmailWorker, job_args)
|
||||
|
||||
emails = Memory.all()
|
||||
admin_notification = Enum.find(emails, fn email ->
|
||||
email.to == [{"", admin_email}]
|
||||
end)
|
||||
|
||||
admin_notification =
|
||||
Enum.find(emails, fn email ->
|
||||
email.to == [{"", admin_email}]
|
||||
end)
|
||||
|
||||
assert admin_notification != nil
|
||||
assert String.contains?(admin_notification.html_body, "Admin Test")
|
||||
|
|
|
|||
|
|
@ -63,7 +63,6 @@ defmodule SpazioSolazzo.BookingSystem.BookingTest do
|
|||
"Test booking"
|
||||
)
|
||||
|
||||
|
||||
assert booking.space_id == space.id
|
||||
assert booking.user_id == nil
|
||||
assert booking.date == date
|
||||
|
|
@ -108,7 +107,6 @@ defmodule SpazioSolazzo.BookingSystem.BookingTest do
|
|||
nil
|
||||
)
|
||||
|
||||
|
||||
error_messages = Ash.Error.error_descriptions(error)
|
||||
assert String.contains?(error_messages, "must be after start time")
|
||||
end
|
||||
|
|
@ -129,7 +127,6 @@ defmodule SpazioSolazzo.BookingSystem.BookingTest do
|
|||
nil
|
||||
)
|
||||
|
||||
|
||||
error_messages = Ash.Error.error_descriptions(error)
|
||||
assert String.contains?(error_messages, "cannot be in the past")
|
||||
end
|
||||
|
|
@ -150,7 +147,6 @@ defmodule SpazioSolazzo.BookingSystem.BookingTest do
|
|||
nil
|
||||
)
|
||||
|
||||
|
||||
assert booking.date == today
|
||||
end
|
||||
|
||||
|
|
@ -183,7 +179,6 @@ defmodule SpazioSolazzo.BookingSystem.BookingTest do
|
|||
nil
|
||||
)
|
||||
|
||||
|
||||
assert booking.customer_phone == nil || booking.customer_phone == ""
|
||||
end
|
||||
end
|
||||
|
|
@ -192,17 +187,16 @@ defmodule SpazioSolazzo.BookingSystem.BookingTest do
|
|||
test "approves a pending booking", %{space: space, date: date} do
|
||||
{:ok, booking} =
|
||||
request_booking(
|
||||
space.id,
|
||||
nil,
|
||||
date,
|
||||
~T[09:00:00],
|
||||
~T[10:00:00],
|
||||
"John Doe",
|
||||
"john@example.com",
|
||||
nil,
|
||||
nil
|
||||
)
|
||||
|
||||
space.id,
|
||||
nil,
|
||||
date,
|
||||
~T[09:00:00],
|
||||
~T[10:00:00],
|
||||
"John Doe",
|
||||
"john@example.com",
|
||||
nil,
|
||||
nil
|
||||
)
|
||||
|
||||
assert booking.state == :requested
|
||||
|
||||
|
|
@ -215,17 +209,16 @@ defmodule SpazioSolazzo.BookingSystem.BookingTest do
|
|||
test "cannot approve already approved booking", %{space: space, date: date} do
|
||||
{:ok, booking} =
|
||||
request_booking(
|
||||
space.id,
|
||||
nil,
|
||||
date,
|
||||
~T[09:00:00],
|
||||
~T[10:00:00],
|
||||
"John Doe",
|
||||
"john@example.com",
|
||||
nil,
|
||||
nil
|
||||
)
|
||||
|
||||
space.id,
|
||||
nil,
|
||||
date,
|
||||
~T[09:00:00],
|
||||
~T[10:00:00],
|
||||
"John Doe",
|
||||
"john@example.com",
|
||||
nil,
|
||||
nil
|
||||
)
|
||||
|
||||
{:ok, _} = BookingSystem.approve_booking(booking.id)
|
||||
|
||||
|
|
@ -237,17 +230,16 @@ defmodule SpazioSolazzo.BookingSystem.BookingTest do
|
|||
test "cannot approve cancelled booking", %{space: space, date: date} do
|
||||
{:ok, booking} =
|
||||
request_booking(
|
||||
space.id,
|
||||
nil,
|
||||
date,
|
||||
~T[09:00:00],
|
||||
~T[10:00:00],
|
||||
"John Doe",
|
||||
"john@example.com",
|
||||
nil,
|
||||
nil
|
||||
)
|
||||
|
||||
space.id,
|
||||
nil,
|
||||
date,
|
||||
~T[09:00:00],
|
||||
~T[10:00:00],
|
||||
"John Doe",
|
||||
"john@example.com",
|
||||
nil,
|
||||
nil
|
||||
)
|
||||
|
||||
{:ok, _} = BookingSystem.cancel_booking(booking.id, "Test cancellation")
|
||||
|
||||
|
|
@ -261,17 +253,16 @@ defmodule SpazioSolazzo.BookingSystem.BookingTest do
|
|||
test "cancels a pending booking", %{space: space, date: date} do
|
||||
{:ok, booking} =
|
||||
request_booking(
|
||||
space.id,
|
||||
nil,
|
||||
date,
|
||||
~T[09:00:00],
|
||||
~T[10:00:00],
|
||||
"John Doe",
|
||||
"john@example.com",
|
||||
nil,
|
||||
nil
|
||||
)
|
||||
|
||||
space.id,
|
||||
nil,
|
||||
date,
|
||||
~T[09:00:00],
|
||||
~T[10:00:00],
|
||||
"John Doe",
|
||||
"john@example.com",
|
||||
nil,
|
||||
nil
|
||||
)
|
||||
|
||||
{:ok, cancelled_booking} = BookingSystem.cancel_booking(booking.id, "Test cancellation")
|
||||
|
||||
|
|
@ -282,17 +273,16 @@ defmodule SpazioSolazzo.BookingSystem.BookingTest do
|
|||
test "cancels an approved booking", %{space: space, date: date} do
|
||||
{:ok, booking} =
|
||||
request_booking(
|
||||
space.id,
|
||||
nil,
|
||||
date,
|
||||
~T[09:00:00],
|
||||
~T[10:00:00],
|
||||
"John Doe",
|
||||
"john@example.com",
|
||||
nil,
|
||||
nil
|
||||
)
|
||||
|
||||
space.id,
|
||||
nil,
|
||||
date,
|
||||
~T[09:00:00],
|
||||
~T[10:00:00],
|
||||
"John Doe",
|
||||
"john@example.com",
|
||||
nil,
|
||||
nil
|
||||
)
|
||||
|
||||
{:ok, _} = BookingSystem.approve_booking(booking.id)
|
||||
{:ok, cancelled_booking} = BookingSystem.cancel_booking(booking.id, "Test cancellation")
|
||||
|
|
@ -303,17 +293,16 @@ defmodule SpazioSolazzo.BookingSystem.BookingTest do
|
|||
test "cannot cancel already cancelled booking", %{space: space, date: date} do
|
||||
{:ok, booking} =
|
||||
request_booking(
|
||||
space.id,
|
||||
nil,
|
||||
date,
|
||||
~T[09:00:00],
|
||||
~T[10:00:00],
|
||||
"John Doe",
|
||||
"john@example.com",
|
||||
nil,
|
||||
nil
|
||||
)
|
||||
|
||||
space.id,
|
||||
nil,
|
||||
date,
|
||||
~T[09:00:00],
|
||||
~T[10:00:00],
|
||||
"John Doe",
|
||||
"john@example.com",
|
||||
nil,
|
||||
nil
|
||||
)
|
||||
|
||||
{:ok, _} = BookingSystem.cancel_booking(booking.id, "Test cancellation")
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,344 @@
|
|||
defmodule SpazioSolazzo.BookingSystem.DuplicateBookingPreventionTest do
|
||||
use SpazioSolazzo.DataCase, async: true
|
||||
|
||||
import SpazioSolazzo.AuthHelpers
|
||||
|
||||
alias SpazioSolazzo.BookingSystem
|
||||
|
||||
setup do
|
||||
{:ok, space} =
|
||||
BookingSystem.create_space(
|
||||
"Coworking",
|
||||
"coworking",
|
||||
"Coworking space",
|
||||
5,
|
||||
10
|
||||
)
|
||||
|
||||
user = register_user("user@example.com", "Test User")
|
||||
|
||||
%{space: space, user: user}
|
||||
end
|
||||
|
||||
describe "duplicate booking prevention" do
|
||||
test "prevents user from requesting duplicate booking when they have a pending request", %{
|
||||
space: space,
|
||||
user: user
|
||||
} do
|
||||
tomorrow = Date.add(Date.utc_today(), 1)
|
||||
start_time = ~T[10:00:00]
|
||||
end_time = ~T[12:00:00]
|
||||
|
||||
{:ok, _first_booking} =
|
||||
BookingSystem.create_booking(
|
||||
space.id,
|
||||
user.id,
|
||||
tomorrow,
|
||||
start_time,
|
||||
end_time,
|
||||
"Test User",
|
||||
"user@example.com",
|
||||
nil,
|
||||
nil
|
||||
)
|
||||
|
||||
result =
|
||||
BookingSystem.create_booking(
|
||||
space.id,
|
||||
user.id,
|
||||
tomorrow,
|
||||
start_time,
|
||||
end_time,
|
||||
"Test User",
|
||||
"user@example.com",
|
||||
nil,
|
||||
nil
|
||||
)
|
||||
|
||||
assert {:error, %Ash.Error.Invalid{}} = result
|
||||
|
||||
assert_error_contains(
|
||||
result,
|
||||
"You already have a pending or confirmed booking for this time slot"
|
||||
)
|
||||
end
|
||||
|
||||
test "prevents user from requesting duplicate booking when they have an accepted booking", %{
|
||||
space: space,
|
||||
user: user
|
||||
} do
|
||||
tomorrow = Date.add(Date.utc_today(), 1)
|
||||
start_time = ~T[10:00:00]
|
||||
end_time = ~T[12:00:00]
|
||||
|
||||
{:ok, first_booking} =
|
||||
BookingSystem.create_booking(
|
||||
space.id,
|
||||
user.id,
|
||||
tomorrow,
|
||||
start_time,
|
||||
end_time,
|
||||
"Test User",
|
||||
"user@example.com",
|
||||
nil,
|
||||
nil
|
||||
)
|
||||
|
||||
{:ok, _approved} = BookingSystem.approve_booking(first_booking)
|
||||
|
||||
result =
|
||||
BookingSystem.create_booking(
|
||||
space.id,
|
||||
user.id,
|
||||
tomorrow,
|
||||
start_time,
|
||||
end_time,
|
||||
"Test User",
|
||||
"user@example.com",
|
||||
nil,
|
||||
nil
|
||||
)
|
||||
|
||||
assert {:error, %Ash.Error.Invalid{}} = result
|
||||
|
||||
assert_error_contains(
|
||||
result,
|
||||
"You already have a pending or confirmed booking for this time slot"
|
||||
)
|
||||
end
|
||||
|
||||
test "allows user to request booking after previous booking was rejected", %{
|
||||
space: space,
|
||||
user: user
|
||||
} do
|
||||
tomorrow = Date.add(Date.utc_today(), 1)
|
||||
start_time = ~T[10:00:00]
|
||||
end_time = ~T[12:00:00]
|
||||
|
||||
{:ok, first_booking} =
|
||||
BookingSystem.create_booking(
|
||||
space.id,
|
||||
user.id,
|
||||
tomorrow,
|
||||
start_time,
|
||||
end_time,
|
||||
"Test User",
|
||||
"user@example.com",
|
||||
nil,
|
||||
nil
|
||||
)
|
||||
|
||||
{:ok, _rejected} = BookingSystem.reject_booking(first_booking, "Sorry, fully booked")
|
||||
|
||||
{:ok, second_booking} =
|
||||
BookingSystem.create_booking(
|
||||
space.id,
|
||||
user.id,
|
||||
tomorrow,
|
||||
start_time,
|
||||
end_time,
|
||||
"Test User",
|
||||
"user@example.com",
|
||||
nil,
|
||||
nil
|
||||
)
|
||||
|
||||
assert second_booking.state == :requested
|
||||
assert second_booking.user_id == user.id
|
||||
end
|
||||
|
||||
test "allows user to request booking after previous booking was cancelled", %{
|
||||
space: space,
|
||||
user: user
|
||||
} do
|
||||
tomorrow = Date.add(Date.utc_today(), 1)
|
||||
start_time = ~T[10:00:00]
|
||||
end_time = ~T[12:00:00]
|
||||
|
||||
{:ok, first_booking} =
|
||||
BookingSystem.create_booking(
|
||||
space.id,
|
||||
user.id,
|
||||
tomorrow,
|
||||
start_time,
|
||||
end_time,
|
||||
"Test User",
|
||||
"user@example.com",
|
||||
nil,
|
||||
nil
|
||||
)
|
||||
|
||||
{:ok, _cancelled} = BookingSystem.cancel_booking(first_booking, "Changed plans")
|
||||
|
||||
{:ok, second_booking} =
|
||||
BookingSystem.create_booking(
|
||||
space.id,
|
||||
user.id,
|
||||
tomorrow,
|
||||
start_time,
|
||||
end_time,
|
||||
"Test User",
|
||||
"user@example.com",
|
||||
nil,
|
||||
nil
|
||||
)
|
||||
|
||||
assert second_booking.state == :requested
|
||||
assert second_booking.user_id == user.id
|
||||
end
|
||||
|
||||
test "prevents overlapping bookings for same user", %{space: space, user: user} do
|
||||
tomorrow = Date.add(Date.utc_today(), 1)
|
||||
|
||||
{:ok, _first_booking} =
|
||||
BookingSystem.create_booking(
|
||||
space.id,
|
||||
user.id,
|
||||
tomorrow,
|
||||
~T[10:00:00],
|
||||
~T[12:00:00],
|
||||
"Test User",
|
||||
"user@example.com",
|
||||
nil,
|
||||
nil
|
||||
)
|
||||
|
||||
result =
|
||||
BookingSystem.create_booking(
|
||||
space.id,
|
||||
user.id,
|
||||
tomorrow,
|
||||
~T[11:00:00],
|
||||
~T[13:00:00],
|
||||
"Test User",
|
||||
"user@example.com",
|
||||
nil,
|
||||
nil
|
||||
)
|
||||
|
||||
assert {:error, %Ash.Error.Invalid{}} = result
|
||||
|
||||
assert_error_contains(
|
||||
result,
|
||||
"You already have a pending or confirmed booking for this time slot"
|
||||
)
|
||||
end
|
||||
|
||||
test "allows different users to book the same time slot", %{space: space, user: user} do
|
||||
another_user = register_user("another@example.com", "Another User")
|
||||
tomorrow = Date.add(Date.utc_today(), 1)
|
||||
start_time = ~T[10:00:00]
|
||||
end_time = ~T[12:00:00]
|
||||
|
||||
{:ok, _first_booking} =
|
||||
BookingSystem.create_booking(
|
||||
space.id,
|
||||
user.id,
|
||||
tomorrow,
|
||||
start_time,
|
||||
end_time,
|
||||
"Test User",
|
||||
"user@example.com",
|
||||
nil,
|
||||
nil
|
||||
)
|
||||
|
||||
{:ok, second_booking} =
|
||||
BookingSystem.create_booking(
|
||||
space.id,
|
||||
another_user.id,
|
||||
tomorrow,
|
||||
start_time,
|
||||
end_time,
|
||||
"Another User",
|
||||
"another@example.com",
|
||||
nil,
|
||||
nil
|
||||
)
|
||||
|
||||
assert second_booking.state == :requested
|
||||
assert second_booking.user_id == another_user.id
|
||||
end
|
||||
|
||||
test "allows user to book different time slots on same day", %{space: space, user: user} do
|
||||
tomorrow = Date.add(Date.utc_today(), 1)
|
||||
|
||||
{:ok, _morning_booking} =
|
||||
BookingSystem.create_booking(
|
||||
space.id,
|
||||
user.id,
|
||||
tomorrow,
|
||||
~T[09:00:00],
|
||||
~T[11:00:00],
|
||||
"Test User",
|
||||
"user@example.com",
|
||||
nil,
|
||||
nil
|
||||
)
|
||||
|
||||
{:ok, afternoon_booking} =
|
||||
BookingSystem.create_booking(
|
||||
space.id,
|
||||
user.id,
|
||||
tomorrow,
|
||||
~T[14:00:00],
|
||||
~T[16:00:00],
|
||||
"Test User",
|
||||
"user@example.com",
|
||||
nil,
|
||||
nil
|
||||
)
|
||||
|
||||
assert afternoon_booking.state == :requested
|
||||
assert afternoon_booking.user_id == user.id
|
||||
end
|
||||
|
||||
test "allows guest bookings without user_id validation", %{space: space} do
|
||||
tomorrow = Date.add(Date.utc_today(), 1)
|
||||
start_time = ~T[10:00:00]
|
||||
end_time = ~T[12:00:00]
|
||||
|
||||
{:ok, _first_booking} =
|
||||
BookingSystem.create_booking(
|
||||
space.id,
|
||||
nil,
|
||||
tomorrow,
|
||||
start_time,
|
||||
end_time,
|
||||
"Guest User",
|
||||
"guest@example.com",
|
||||
nil,
|
||||
nil
|
||||
)
|
||||
|
||||
{:ok, second_booking} =
|
||||
BookingSystem.create_booking(
|
||||
space.id,
|
||||
nil,
|
||||
tomorrow,
|
||||
start_time,
|
||||
end_time,
|
||||
"Another Guest",
|
||||
"another.guest@example.com",
|
||||
nil,
|
||||
nil
|
||||
)
|
||||
|
||||
assert second_booking.state == :requested
|
||||
assert second_booking.user_id == nil
|
||||
end
|
||||
end
|
||||
|
||||
defp assert_error_contains({:error, %Ash.Error.Invalid{errors: errors}}, expected_message) do
|
||||
error_messages =
|
||||
Enum.map(errors, fn error ->
|
||||
case error do
|
||||
%{message: message} -> message
|
||||
_ -> inspect(error)
|
||||
end
|
||||
end)
|
||||
|
||||
assert Enum.any?(error_messages, &String.contains?(&1, expected_message)),
|
||||
"Expected error message to contain '#{expected_message}', but got: #{inspect(error_messages)}"
|
||||
end
|
||||
end
|
||||
|
|
@ -66,21 +66,31 @@ defmodule SpazioSolazzo.BookingSystem.MultiDayBookingTest do
|
|||
)
|
||||
|
||||
# Should appear on start date
|
||||
{:ok, day1_bookings} = BookingSystem.list_accepted_space_bookings_by_date(space.id, 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)
|
||||
|
||||
{: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)
|
||||
{: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)
|
||||
|
||||
{:ok, day_after_bookings} =
|
||||
BookingSystem.list_accepted_space_bookings_by_date(space.id, day_after)
|
||||
|
||||
assert length(day_after_bookings) == 0
|
||||
end
|
||||
|
||||
|
|
@ -291,11 +301,16 @@ defmodule SpazioSolazzo.BookingSystem.MultiDayBookingTest do
|
|||
|
||||
# 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)
|
||||
|
||||
{: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)
|
||||
{:ok, bookings_start} =
|
||||
BookingSystem.list_accepted_space_bookings_by_date(space.id, start_date)
|
||||
|
||||
assert length(bookings_start) == 1
|
||||
|
||||
# Should appear on end date
|
||||
|
|
@ -304,7 +319,10 @@ defmodule SpazioSolazzo.BookingSystem.MultiDayBookingTest do
|
|||
|
||||
# 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)
|
||||
|
||||
{:ok, bookings_after} =
|
||||
BookingSystem.list_accepted_space_bookings_by_date(space.id, day_after)
|
||||
|
||||
assert length(bookings_after) == 0
|
||||
end
|
||||
|
||||
|
|
@ -338,7 +356,10 @@ defmodule SpazioSolazzo.BookingSystem.MultiDayBookingTest do
|
|||
|
||||
# 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)
|
||||
|
||||
{:ok, bookings_after} =
|
||||
BookingSystem.list_accepted_space_bookings_by_date(space.id, day_after)
|
||||
|
||||
assert length(bookings_after) == 0
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -0,0 +1,289 @@
|
|||
defmodule SpazioSolazzoWeb.Admin.BookingManagementRejectionTest do
|
||||
use SpazioSolazzoWeb.ConnCase, async: true
|
||||
|
||||
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
|
||||
|
||||
defp create_booking(space, user) do
|
||||
tomorrow = Date.add(Date.utc_today(), 1)
|
||||
start_time = ~T[10:00:00]
|
||||
end_time = ~T[12:00:00]
|
||||
|
||||
{:ok, booking} =
|
||||
BookingSystem.create_booking(
|
||||
space.id,
|
||||
user.id,
|
||||
tomorrow,
|
||||
start_time,
|
||||
end_time,
|
||||
"Test User",
|
||||
"test@example.com",
|
||||
"+1234567890",
|
||||
"Test booking comment"
|
||||
)
|
||||
|
||||
booking
|
||||
end
|
||||
|
||||
setup do
|
||||
{:ok, space} =
|
||||
BookingSystem.create_space(
|
||||
"Coworking",
|
||||
"coworking",
|
||||
"Coworking space",
|
||||
5,
|
||||
10
|
||||
)
|
||||
|
||||
admin_user = create_admin_user()
|
||||
regular_user = register_user("user@example.com", "Regular User")
|
||||
|
||||
%{space: space, admin_user: admin_user, regular_user: regular_user}
|
||||
end
|
||||
|
||||
describe "booking rejection modal" do
|
||||
test "shows reject modal when clicking reject button", %{
|
||||
conn: conn,
|
||||
admin_user: admin_user,
|
||||
space: space,
|
||||
regular_user: regular_user
|
||||
} do
|
||||
booking = create_booking(space, regular_user)
|
||||
conn = log_in_user(conn, admin_user)
|
||||
{:ok, view, _html} = live(conn, "/admin/bookings")
|
||||
|
||||
refute has_element?(view, "#success-modal")
|
||||
|
||||
html =
|
||||
view
|
||||
|> element("button[phx-click='show_reject_modal'][phx-value-booking_id='#{booking.id}']")
|
||||
|> render_click()
|
||||
|
||||
assert html =~ "Reject Booking"
|
||||
assert html =~ "Rejection Reason"
|
||||
end
|
||||
|
||||
test "hides reject modal when clicking cancel", %{
|
||||
conn: conn,
|
||||
admin_user: admin_user,
|
||||
space: space,
|
||||
regular_user: regular_user
|
||||
} do
|
||||
booking = create_booking(space, regular_user)
|
||||
conn = log_in_user(conn, admin_user)
|
||||
{:ok, view, _html} = live(conn, "/admin/bookings")
|
||||
|
||||
view
|
||||
|> element("button[phx-click='show_reject_modal'][phx-value-booking_id='#{booking.id}']")
|
||||
|> render_click()
|
||||
|
||||
html =
|
||||
view
|
||||
|> element("button[phx-click='hide_reject_modal']")
|
||||
|> render_click()
|
||||
|
||||
refute html =~ "Reject Booking"
|
||||
end
|
||||
|
||||
test "shows error when rejection reason is empty", %{
|
||||
conn: conn,
|
||||
admin_user: admin_user,
|
||||
space: space,
|
||||
regular_user: regular_user
|
||||
} do
|
||||
booking = create_booking(space, regular_user)
|
||||
conn = log_in_user(conn, admin_user)
|
||||
{:ok, view, _html} = live(conn, "/admin/bookings")
|
||||
|
||||
view
|
||||
|> element("button[phx-click='show_reject_modal'][phx-value-booking_id='#{booking.id}']")
|
||||
|> render_click()
|
||||
|
||||
html =
|
||||
view
|
||||
|> element("form[phx-submit='confirm_reject']")
|
||||
|> render_submit(%{"reason" => ""})
|
||||
|
||||
assert html =~ "Please provide a rejection reason"
|
||||
end
|
||||
|
||||
test "successfully rejects booking with valid reason", %{
|
||||
conn: conn,
|
||||
admin_user: admin_user,
|
||||
space: space,
|
||||
regular_user: regular_user
|
||||
} do
|
||||
booking = create_booking(space, regular_user)
|
||||
conn = log_in_user(conn, admin_user)
|
||||
{:ok, view, _html} = live(conn, "/admin/bookings")
|
||||
|
||||
view
|
||||
|> element("button[phx-click='show_reject_modal'][phx-value-booking_id='#{booking.id}']")
|
||||
|> render_click()
|
||||
|
||||
view
|
||||
|> element("textarea[name='reason']")
|
||||
|> render_change(%{"reason" => "Space under maintenance"})
|
||||
|
||||
html =
|
||||
view
|
||||
|> element("form[phx-submit='confirm_reject']")
|
||||
|> render_submit()
|
||||
|
||||
assert html =~ "Booking rejected"
|
||||
|
||||
{:ok, updated_booking} = Ash.get(SpazioSolazzo.BookingSystem.Booking, booking.id)
|
||||
assert updated_booking.state == :rejected
|
||||
assert updated_booking.rejection_reason == "Space under maintenance"
|
||||
end
|
||||
|
||||
test "updates rejection reason as user types", %{
|
||||
conn: conn,
|
||||
admin_user: admin_user,
|
||||
space: space,
|
||||
regular_user: regular_user
|
||||
} do
|
||||
booking = create_booking(space, regular_user)
|
||||
conn = log_in_user(conn, admin_user)
|
||||
{:ok, view, _html} = live(conn, "/admin/bookings")
|
||||
|
||||
view
|
||||
|> element("button[phx-click='show_reject_modal'][phx-value-booking_id='#{booking.id}']")
|
||||
|> render_click()
|
||||
|
||||
html =
|
||||
view
|
||||
|> element("textarea[name='reason']")
|
||||
|> render_change(%{"reason" => "Fully booked"})
|
||||
|
||||
assert html =~ "Fully booked"
|
||||
end
|
||||
|
||||
test "closes modal after successful rejection", %{
|
||||
conn: conn,
|
||||
admin_user: admin_user,
|
||||
space: space,
|
||||
regular_user: regular_user
|
||||
} do
|
||||
booking = create_booking(space, regular_user)
|
||||
conn = log_in_user(conn, admin_user)
|
||||
{:ok, view, _html} = live(conn, "/admin/bookings")
|
||||
|
||||
view
|
||||
|> element("button[phx-click='show_reject_modal'][phx-value-booking_id='#{booking.id}']")
|
||||
|> render_click()
|
||||
|
||||
view
|
||||
|> element("textarea[name='reason']")
|
||||
|> render_change(%{"reason" => "Not available"})
|
||||
|
||||
html =
|
||||
view
|
||||
|> element("form[phx-submit='confirm_reject']")
|
||||
|> render_submit()
|
||||
|
||||
refute html =~ "Reject Booking"
|
||||
assert html =~ "Booking rejected"
|
||||
end
|
||||
|
||||
test "rejected booking moves from pending to past bookings", %{
|
||||
conn: conn,
|
||||
admin_user: admin_user,
|
||||
space: space,
|
||||
regular_user: regular_user
|
||||
} do
|
||||
booking = create_booking(space, regular_user)
|
||||
conn = log_in_user(conn, admin_user)
|
||||
{:ok, view, html} = live(conn, "/admin/bookings")
|
||||
|
||||
assert html =~ "Pending"
|
||||
|
||||
view
|
||||
|> element("button[phx-click='show_reject_modal'][phx-value-booking_id='#{booking.id}']")
|
||||
|> render_click()
|
||||
|
||||
view
|
||||
|> element("textarea[name='reason']")
|
||||
|> render_change(%{"reason" => "Not available"})
|
||||
|
||||
_html =
|
||||
view
|
||||
|> element("form[phx-submit='confirm_reject']")
|
||||
|> render_submit()
|
||||
|
||||
{:ok, updated_booking} = Ash.get(SpazioSolazzo.BookingSystem.Booking, booking.id)
|
||||
assert updated_booking.state == :rejected
|
||||
assert updated_booking.rejection_reason == "Not available"
|
||||
end
|
||||
|
||||
test "multiple bookings can be rejected independently", %{
|
||||
conn: conn,
|
||||
admin_user: admin_user,
|
||||
space: space,
|
||||
regular_user: regular_user
|
||||
} do
|
||||
tomorrow = Date.add(Date.utc_today(), 1)
|
||||
|
||||
{:ok, booking1} =
|
||||
BookingSystem.create_booking(
|
||||
space.id,
|
||||
regular_user.id,
|
||||
tomorrow,
|
||||
~T[09:00:00],
|
||||
~T[11:00:00],
|
||||
"Test User",
|
||||
"test@example.com",
|
||||
"+1234567890",
|
||||
"First booking"
|
||||
)
|
||||
|
||||
{:ok, booking2} =
|
||||
BookingSystem.create_booking(
|
||||
space.id,
|
||||
regular_user.id,
|
||||
tomorrow,
|
||||
~T[14:00:00],
|
||||
~T[16:00:00],
|
||||
"Test User",
|
||||
"test@example.com",
|
||||
"+1234567890",
|
||||
"Second booking"
|
||||
)
|
||||
|
||||
conn = log_in_user(conn, admin_user)
|
||||
{:ok, view, _html} = live(conn, "/admin/bookings")
|
||||
|
||||
view
|
||||
|> element("button[phx-click='show_reject_modal'][phx-value-booking_id='#{booking1.id}']")
|
||||
|> render_click()
|
||||
|
||||
view
|
||||
|> element("textarea[name='reason']")
|
||||
|> render_change(%{"reason" => "Reason 1"})
|
||||
|
||||
view
|
||||
|> element("form[phx-submit='confirm_reject']")
|
||||
|> render_submit()
|
||||
|
||||
{:ok, updated_booking1} = Ash.get(SpazioSolazzo.BookingSystem.Booking, booking1.id)
|
||||
{:ok, updated_booking2} = Ash.get(SpazioSolazzo.BookingSystem.Booking, booking2.id)
|
||||
|
||||
assert updated_booking1.state == :rejected
|
||||
assert updated_booking1.rejection_reason == "Reason 1"
|
||||
assert updated_booking2.state == :requested
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -11,8 +11,10 @@ defmodule SpazioSolazzoWeb.Admin.WalkInLiveSimpleTest do
|
|||
user = register_user("admin@example.com", "Admin User")
|
||||
# Directly update role to admin using Ecto
|
||||
{:ok, uuid} = Ecto.UUID.dump(user.id)
|
||||
|
||||
from(u in "users", where: u.id == ^uuid)
|
||||
|> SpazioSolazzo.Repo.update_all(set: [role: "admin"])
|
||||
|
||||
user
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -10,8 +10,10 @@ defmodule SpazioSolazzoWeb.Admin.WalkInLiveTest do
|
|||
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
|
||||
|
||||
|
|
@ -41,7 +43,11 @@ defmodule SpazioSolazzoWeb.Admin.WalkInLiveTest do
|
|||
assert has_element?(view, "button[phx-click='create_booking']")
|
||||
end
|
||||
|
||||
test "creates single-day walk-in booking successfully", %{conn: conn, user: user, space: space} do
|
||||
test "creates single-day walk-in booking successfully", %{
|
||||
conn: conn,
|
||||
user: user,
|
||||
space: space
|
||||
} do
|
||||
conn = log_in_user(conn, user)
|
||||
{:ok, view, _html} = live(conn, "/admin/walk-in")
|
||||
|
||||
|
|
@ -180,7 +186,9 @@ defmodule SpazioSolazzoWeb.Admin.WalkInLiveTest do
|
|||
|
||||
assert length(day2_bookings) == 1
|
||||
|
||||
{:ok, day3_bookings} = BookingSystem.list_accepted_space_bookings_by_date(space.id, end_date)
|
||||
{:ok, day3_bookings} =
|
||||
BookingSystem.list_accepted_space_bookings_by_date(space.id, end_date)
|
||||
|
||||
assert length(day3_bookings) == 1
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -50,9 +50,18 @@ defmodule SpazioSolazzoWeb.BookingLive.SpaceBookingTest do
|
|||
)
|
||||
|
||||
user = register_user("testuser@example.com", "Test User", "+39 1234567890")
|
||||
unauth_conn = conn
|
||||
conn = log_in_user(conn, user)
|
||||
|
||||
%{conn: conn, space: space, slot1: slot1, slot2: slot2, today: today, user: user}
|
||||
%{
|
||||
conn: conn,
|
||||
unauth_conn: unauth_conn,
|
||||
space: space,
|
||||
slot1: slot1,
|
||||
slot2: slot2,
|
||||
today: today,
|
||||
user: user
|
||||
}
|
||||
end
|
||||
|
||||
defp day_of_week_atom(date) do
|
||||
|
|
@ -379,6 +388,9 @@ defmodule SpazioSolazzoWeb.BookingLive.SpaceBookingTest do
|
|||
|> element("#booking-form")
|
||||
|> render_submit(form_data)
|
||||
|
||||
# Process the handle_info message that creates the booking
|
||||
render(view)
|
||||
|
||||
{:ok, bookings} = BookingSystem.list_booking_requests(space.id, nil, today)
|
||||
|
||||
assert length(bookings) == 1
|
||||
|
|
@ -560,6 +572,9 @@ defmodule SpazioSolazzoWeb.BookingLive.SpaceBookingTest do
|
|||
|> element("#booking-form")
|
||||
|> render_submit(form_data)
|
||||
|
||||
# Process the handle_info message that creates the booking
|
||||
render(view)
|
||||
|
||||
{:ok, bookings} = BookingSystem.list_booking_requests(space.id, nil, today)
|
||||
|
||||
assert length(bookings) == 1
|
||||
|
|
@ -570,52 +585,9 @@ defmodule SpazioSolazzoWeb.BookingLive.SpaceBookingTest do
|
|||
end
|
||||
|
||||
describe "SpaceBooking edge cases" do
|
||||
test "handles multiple concurrent users booking same slot", %{
|
||||
slot1: slot1,
|
||||
conn: conn,
|
||||
space: space,
|
||||
today: today
|
||||
} do
|
||||
{:ok, view1, _html} = live(conn, ~p"/book/space/#{space.slug}")
|
||||
{:ok, view2, _html} = live(conn, ~p"/book/space/#{space.slug}")
|
||||
|
||||
view1
|
||||
|> element("button[phx-click='select_slot'][phx-value-time_slot_id='#{slot1.id}']")
|
||||
|> render_click()
|
||||
|
||||
view2
|
||||
|> element("button[phx-click='select_slot'][phx-value-time_slot_id='#{slot1.id}']")
|
||||
|> render_click()
|
||||
|
||||
form_data1 = %{
|
||||
"customer_name" => "User 1",
|
||||
"customer_email" => "user1@example.com",
|
||||
"customer_phone" => "",
|
||||
"customer_comment" => ""
|
||||
}
|
||||
|
||||
form_data2 = %{
|
||||
"customer_name" => "User 2",
|
||||
"customer_email" => "user2@example.com",
|
||||
"customer_phone" => "",
|
||||
"customer_comment" => ""
|
||||
}
|
||||
|
||||
view1
|
||||
|> element("#booking-form")
|
||||
|> render_submit(form_data1)
|
||||
|
||||
view2
|
||||
|> element("#booking-form")
|
||||
|> render_submit(form_data2)
|
||||
|
||||
# Wait for async booking creation to complete
|
||||
Process.sleep(100)
|
||||
|
||||
{:ok, bookings} = BookingSystem.list_booking_requests(space.id, nil, today)
|
||||
|
||||
assert length(bookings) == 2
|
||||
end
|
||||
# Note: This test was removed because duplicate booking prevention now correctly
|
||||
# blocks the same user from booking the same slot twice, which is the expected behavior.
|
||||
# Duplicate booking prevention is thoroughly tested in duplicate_booking_prevention_test.exs
|
||||
|
||||
test "shows high demand when public capacity is reached", %{conn: conn} do
|
||||
{:ok, small_space} =
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue