refactor: booking start time and end time

This commit is contained in:
JasterV 2026-02-02 01:25:24 +01:00
parent 9136c9ae6d
commit b48af6d3e6
23 changed files with 1098 additions and 158 deletions

View file

@ -46,10 +46,18 @@ defmodule SpazioSolazzo.BookingSystem.Booking do
argument :space_id, :uuid, allow_nil?: false
argument :date, :date, allow_nil?: false
filter expr(
space_id == ^arg(:space_id) and date == ^arg(:date) and
state == :accepted
)
prepare fn query, _ctx ->
date = Ash.Query.get_argument(query, :date)
day_start = DateTime.new!(date, ~T[00:00:00], "Etc/UTC")
day_end = DateTime.new!(date, ~T[23:59:59], "Etc/UTC")
query
|> Ash.Query.filter(
start_datetime < ^day_end and end_datetime > ^day_start and state == :accepted
)
end
filter expr(space_id == ^arg(:space_id))
end
read :list_booking_requests do
@ -73,8 +81,14 @@ defmodule SpazioSolazzo.BookingSystem.Booking do
end
case Ash.Query.get_argument(query, :date) do
nil -> query
date -> Ash.Query.filter(query, date == ^date)
nil ->
query
date ->
day_start = DateTime.new!(date, ~T[00:00:00], "Etc/UTC")
day_end = DateTime.new!(date, ~T[23:59:59], "Etc/UTC")
Ash.Query.filter(query, start_datetime < ^day_end and end_datetime > ^day_start)
end
end
end
@ -83,7 +97,6 @@ defmodule SpazioSolazzo.BookingSystem.Booking do
filter expr(state == :requested)
end
create :create do
argument :space_id, :uuid, allow_nil?: false
argument :user_id, :uuid, allow_nil?: true
@ -132,19 +145,19 @@ defmodule SpazioSolazzo.BookingSystem.Booking do
end
change fn changeset, _ctx ->
date = Ash.Changeset.get_argument(changeset, :date)
start_time = Ash.Changeset.get_argument(changeset, :start_time)
end_time = Ash.Changeset.get_argument(changeset, :end_time)
start_datetime = DateTime.new!(date, start_time, "Etc/UTC")
end_datetime = DateTime.new!(date, end_time, "Etc/UTC")
changeset
|> Ash.Changeset.force_change_attribute(
:date,
Ash.Changeset.get_argument(changeset, :date)
)
|> Ash.Changeset.force_change_attribute(
:start_time,
Ash.Changeset.get_argument(changeset, :start_time)
)
|> Ash.Changeset.force_change_attribute(
:end_time,
Ash.Changeset.get_argument(changeset, :end_time)
)
|> Ash.Changeset.force_change_attribute(:start_datetime, start_datetime)
|> Ash.Changeset.force_change_attribute(:end_datetime, end_datetime)
|> Ash.Changeset.force_change_attribute(:date, date)
|> Ash.Changeset.force_change_attribute(:start_time, start_time)
|> Ash.Changeset.force_change_attribute(:end_time, end_time)
|> Ash.Changeset.force_change_attribute(
:customer_name,
Ash.Changeset.get_argument(changeset, :customer_name)
@ -173,9 +186,8 @@ defmodule SpazioSolazzo.BookingSystem.Booking do
customer_phone: booking.customer_phone,
customer_comment: booking.customer_comment,
space_name: booking.space.name,
date: Calendar.strftime(booking.date, "%A, %B %d"),
start_time: booking.start_time,
end_time: booking.end_time
start_datetime: booking.start_datetime,
end_datetime: booking.end_datetime
}
|> RequestCreatedEmailWorker.new()
|> Oban.insert!()
@ -237,6 +249,8 @@ defmodule SpazioSolazzo.BookingSystem.Booking do
end_time = DateTime.to_time(end_datetime)
changeset
|> Ash.Changeset.force_change_attribute(:start_datetime, start_datetime)
|> Ash.Changeset.force_change_attribute(:end_datetime, end_datetime)
|> Ash.Changeset.force_change_attribute(:date, date)
|> Ash.Changeset.force_change_attribute(:start_time, start_time)
|> Ash.Changeset.force_change_attribute(:end_time, end_time)
@ -274,9 +288,8 @@ defmodule SpazioSolazzo.BookingSystem.Booking do
customer_email: booking.customer_email,
customer_phone: booking.customer_phone,
space_name: booking.space.name,
date: Calendar.strftime(booking.date, "%A, %B %d"),
start_time: booking.start_time,
end_time: booking.end_time,
start_datetime: booking.start_datetime,
end_datetime: booking.end_datetime,
action: "accepted"
}
|> AdminActionEmailWorker.new()
@ -306,9 +319,8 @@ defmodule SpazioSolazzo.BookingSystem.Booking do
customer_email: booking.customer_email,
customer_phone: booking.customer_phone,
space_name: booking.space.name,
date: Calendar.strftime(booking.date, "%A, %B %d"),
start_time: booking.start_time,
end_time: booking.end_time,
start_datetime: booking.start_datetime,
end_datetime: booking.end_datetime,
action: "rejected",
rejection_reason: booking.rejection_reason
}
@ -339,9 +351,8 @@ defmodule SpazioSolazzo.BookingSystem.Booking do
customer_email: booking.customer_email,
customer_phone: booking.customer_phone,
space_name: booking.space.name,
date: Calendar.strftime(booking.date, "%A, %B %d"),
start_time: booking.start_time,
end_time: booking.end_time,
start_datetime: booking.start_datetime,
end_datetime: booking.end_datetime,
cancellation_reason: booking.cancellation_reason
}
|> UserCancellationEmailWorker.new()
@ -368,10 +379,15 @@ defmodule SpazioSolazzo.BookingSystem.Booking do
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(date == ^date)
|> Ash.Query.filter(state == :requested or state == :accepted)
|> Ash.Query.filter(
start_datetime < ^end_datetime and end_datetime > ^start_datetime and
(state == :requested or state == :accepted)
)
query =
if space_id do
@ -382,15 +398,8 @@ defmodule SpazioSolazzo.BookingSystem.Booking do
case Ash.read(query) do
{:ok, bookings} ->
# Filter overlapping bookings
overlapping_bookings =
Enum.filter(bookings, fn booking ->
Time.compare(booking.start_time, end_time) == :lt and
Time.compare(start_time, booking.end_time) == :lt
end)
pending_count = Enum.count(overlapping_bookings, &(&1.state == :requested))
approved_count = Enum.count(overlapping_bookings, &(&1.state == :accepted))
pending_count = Enum.count(bookings, &(&1.state == :requested))
approved_count = Enum.count(bookings, &(&1.state == :accepted))
{:ok, %{pending: pending_count, approved: approved_count}}
@ -431,6 +440,8 @@ defmodule SpazioSolazzo.BookingSystem.Booking do
attributes do
uuid_primary_key :id
attribute :start_datetime, :datetime, allow_nil?: false
attribute :end_datetime, :datetime, allow_nil?: false
attribute :date, :date, allow_nil?: false
attribute :customer_name, :string, allow_nil?: false
attribute :customer_email, :string, allow_nil?: false

View file

@ -6,6 +6,7 @@ defmodule SpazioSolazzo.BookingSystem.Booking.AdminActionEmailWorker do
use Oban.Worker, queue: :booking_email, max_attempts: 3
alias SpazioSolazzo.BookingSystem.Booking.Email
alias SpazioSolazzo.CalendarExt
@impl Oban.Worker
def perform(%Oban.Job{
@ -15,21 +16,25 @@ defmodule SpazioSolazzo.BookingSystem.Booking.AdminActionEmailWorker do
"customer_email" => customer_email,
"customer_phone" => customer_phone,
"space_name" => space_name,
"date" => date,
"start_time" => start_time,
"end_time" => end_time,
"start_datetime" => start_datetime_str,
"end_datetime" => end_datetime_str,
"action" => "accepted"
}
}) do
{:ok, start_datetime, _} = DateTime.from_iso8601(start_datetime_str)
{:ok, end_datetime, _} = DateTime.from_iso8601(end_datetime_str)
%{
booking_id: booking_id,
customer_name: customer_name,
customer_email: customer_email,
customer_phone: customer_phone,
space_name: space_name,
date: date,
start_time: start_time,
end_time: end_time
start_datetime: start_datetime,
end_datetime: end_datetime,
date: CalendarExt.format_datetime_date_only(start_datetime),
start_time: DateTime.to_time(start_datetime),
end_time: DateTime.to_time(end_datetime)
}
|> Email.booking_request_approved()
|> SpazioSolazzo.Mailer.deliver!()
@ -43,20 +48,24 @@ defmodule SpazioSolazzo.BookingSystem.Booking.AdminActionEmailWorker do
"customer_name" => customer_name,
"customer_email" => customer_email,
"space_name" => space_name,
"date" => date,
"start_time" => start_time,
"end_time" => end_time,
"start_datetime" => start_datetime_str,
"end_datetime" => end_datetime_str,
"action" => "rejected",
"rejection_reason" => rejection_reason
}
}) do
{:ok, start_datetime, _} = DateTime.from_iso8601(start_datetime_str)
{:ok, end_datetime, _} = DateTime.from_iso8601(end_datetime_str)
%{
customer_name: customer_name,
customer_email: customer_email,
space_name: space_name,
date: date,
start_time: start_time,
end_time: end_time,
start_datetime: start_datetime,
end_datetime: end_datetime,
date: CalendarExt.format_datetime_date_only(start_datetime),
start_time: DateTime.to_time(start_datetime),
end_time: DateTime.to_time(end_datetime),
rejection_reason: rejection_reason
}
|> Email.booking_request_rejected()

View file

@ -7,6 +7,7 @@ defmodule SpazioSolazzo.BookingSystem.Booking.RequestCreatedEmailWorker do
use Oban.Worker, queue: :booking_email, max_attempts: 3
alias SpazioSolazzo.BookingSystem.Booking.Email
alias SpazioSolazzo.CalendarExt
@impl Oban.Worker
def perform(%Oban.Job{
@ -17,11 +18,13 @@ defmodule SpazioSolazzo.BookingSystem.Booking.RequestCreatedEmailWorker do
"customer_phone" => customer_phone,
"customer_comment" => customer_comment,
"space_name" => space_name,
"date" => date,
"start_time" => start_time,
"end_time" => end_time
"start_datetime" => start_datetime_str,
"end_datetime" => end_datetime_str
}
}) do
{:ok, start_datetime, _} = DateTime.from_iso8601(start_datetime_str)
{:ok, end_datetime, _} = DateTime.from_iso8601(end_datetime_str)
email_data = %{
booking_id: booking_id,
customer_name: customer_name,
@ -29,9 +32,11 @@ defmodule SpazioSolazzo.BookingSystem.Booking.RequestCreatedEmailWorker do
customer_phone: customer_phone,
customer_comment: customer_comment,
space_name: space_name,
date: date,
start_time: start_time,
end_time: end_time,
start_datetime: start_datetime,
end_datetime: end_datetime,
date: CalendarExt.format_datetime_date_only(start_datetime),
start_time: DateTime.to_time(start_datetime),
end_time: DateTime.to_time(end_datetime),
admin_email: admin_email()
}

View file

@ -6,6 +6,7 @@ defmodule SpazioSolazzo.BookingSystem.Booking.UserCancellationEmailWorker do
use Oban.Worker, queue: :booking_email, max_attempts: 3
alias SpazioSolazzo.BookingSystem.Booking.Email
alias SpazioSolazzo.CalendarExt
@impl Oban.Worker
def perform(%Oban.Job{
@ -14,20 +15,24 @@ defmodule SpazioSolazzo.BookingSystem.Booking.UserCancellationEmailWorker do
"customer_email" => customer_email,
"customer_phone" => customer_phone,
"space_name" => space_name,
"date" => date,
"start_time" => start_time,
"end_time" => end_time,
"start_datetime" => start_datetime_str,
"end_datetime" => end_datetime_str,
"cancellation_reason" => cancellation_reason
}
}) do
{:ok, start_datetime, _} = DateTime.from_iso8601(start_datetime_str)
{:ok, end_datetime, _} = DateTime.from_iso8601(end_datetime_str)
%{
customer_name: customer_name,
customer_email: customer_email,
customer_phone: customer_phone,
space_name: space_name,
date: date,
start_time: start_time,
end_time: end_time,
start_datetime: start_datetime,
end_datetime: end_datetime,
date: CalendarExt.format_datetime_date_only(start_datetime),
start_time: DateTime.to_time(start_datetime),
end_time: DateTime.to_time(end_datetime),
cancellation_reason: cancellation_reason,
admin_email: admin_email()
}

View file

@ -55,25 +55,24 @@ defmodule SpazioSolazzo.BookingSystem.Space do
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 on the given date
# 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 date == ^date_arg and state == :accepted)
expr(
space_id == ^space_id and state == :accepted and start_datetime < ^end_datetime and
end_datetime > ^start_datetime
)
)
case Ash.read(query) do
{:ok, bookings} ->
# Filter overlapping bookings
overlapping_bookings =
Enum.filter(bookings, fn booking ->
Time.compare(booking.start_time, end_time_arg) == :lt and
Time.compare(start_time_arg, booking.end_time) == :lt
end)
{:ok, overlapping_bookings} ->
current_count = length(overlapping_bookings)
availability =

View file

@ -13,4 +13,99 @@ defmodule SpazioSolazzo.CalendarExt do
"#{start_time} - #{end_time}"
end
@doc """
Formats a datetime as "Feb 10, 2026"
"""
def format_datetime_date(%DateTime{} = datetime) do
Calendar.strftime(datetime, "%b %d, %Y")
end
@doc """
Formats a datetime as "Monday, February 10, 2026"
"""
def format_datetime_date_long(%DateTime{} = datetime) do
Calendar.strftime(datetime, "%A, %B %d, %Y")
end
@doc """
Formats a datetime as "Monday, February 10" (for emails)
"""
def format_datetime_date_only(%DateTime{} = datetime) do
Calendar.strftime(datetime, "%A, %B %d")
end
@doc """
Formats a time or datetime as "9:00 AM"
"""
def format_time(%DateTime{} = datetime) do
datetime
|> DateTime.to_time()
|> format_time()
end
def format_time(%Time{} = time) do
Calendar.strftime(time, "%I:%M %p")
end
@doc """
Formats a time range as "9:00 AM - 5:00 PM"
Takes two Time or DateTime structs
"""
def format_time_range(%DateTime{} = start_dt, %DateTime{} = end_dt) do
start_time = DateTime.to_time(start_dt)
end_time = DateTime.to_time(end_dt)
format_time_range(start_time, end_time)
end
def format_time_range(%Time{} = start_time, %Time{} = end_time) do
"#{format_time(start_time)} - #{format_time(end_time)}"
end
@doc """
Checks if a booking spans multiple days
"""
def is_multi_day?(%DateTime{} = start_datetime, %DateTime{} = end_datetime) do
start_date = DateTime.to_date(start_datetime)
end_date = DateTime.to_date(end_datetime)
Date.compare(start_date, end_date) != :eq
end
@doc """
Formats a datetime range handling both single-day and multi-day bookings.
Single-day: "Feb 10, 2026 9:00 AM - 5:00 PM"
Multi-day: "Feb 10, 2026 9:00 AM - Feb 15, 2026 5:00 PM"
"""
def format_datetime_range(%DateTime{} = start_datetime, %DateTime{} = end_datetime) do
if is_multi_day?(start_datetime, end_datetime) do
"#{format_datetime_date(start_datetime)} #{format_time(start_datetime)} - #{format_datetime_date(end_datetime)} #{format_time(end_datetime)}"
else
"#{format_datetime_date(start_datetime)} #{format_time_range(start_datetime, end_datetime)}"
end
end
@doc """
Formats the start portion of a datetime range for table display
Single-day: "Feb 10, 2026 9:00 AM"
Multi-day: "Feb 10, 2026 9:00 AM"
"""
def format_datetime_range_start(%DateTime{} = datetime) do
"#{format_datetime_date(datetime)} #{format_time(datetime)}"
end
@doc """
Formats the end portion of a datetime range for table display
Single-day: "5:00 PM" (date not shown)
Multi-day: "Feb 15, 2026 5:00 PM"
"""
def format_datetime_range_end(%DateTime{} = start_datetime, %DateTime{} = end_datetime) do
if is_multi_day?(start_datetime, end_datetime) do
"#{format_datetime_date(end_datetime)} #{format_time(end_datetime)}"
else
format_time(end_datetime)
end
end
end

View file

@ -56,8 +56,9 @@ defmodule SpazioSolazzoWeb.Admin.AdminCalendarComponent do
{:noreply, socket}
end
def handle_event("toggle_multi_day", %{"value" => value}, socket) do
multi_day = value == "on"
def handle_event("toggle_multi_day", _params, socket) do
# Toggle the current state
multi_day = !socket.assigns.multi_day_mode
socket =
socket
@ -238,12 +239,13 @@ defmodule SpazioSolazzoWeb.Admin.AdminCalendarComponent do
defp is_end_date?(_date, _, nil), do: false
defp is_end_date?(date, _, end_date), do: Date.compare(date, end_date) == :eq
defp day_classes(date, socket) do
capacity = Map.get(socket.assigns.day_capacities, date, :available)
defp day_classes(date, assigns) do
# Extract capacity status for the given date
capacity = Map.get(assigns.day_capacities, date, :available)
is_past = Date.compare(date, Date.utc_today()) == :lt
in_range = day_in_range?(date, socket.assigns.selected_date, socket.assigns.start_date, socket.assigns.end_date)
is_start = is_start_date?(date, socket.assigns.start_date, socket.assigns.end_date)
is_end = is_end_date?(date, socket.assigns.start_date, socket.assigns.end_date)
in_range = day_in_range?(date, assigns.selected_date, assigns.start_date, assigns.end_date)
is_start = is_start_date?(date, assigns.start_date, assigns.end_date)
is_end = is_end_date?(date, assigns.start_date, assigns.end_date)
base = "aspect-square flex flex-col items-center justify-center transition-all"
@ -254,7 +256,7 @@ defmodule SpazioSolazzoWeb.Admin.AdminCalendarComponent do
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"]
in_range && socket.assigns.multi_day_mode && socket.assigns.end_date != nil ->
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"]

View file

@ -1,15 +1,18 @@
<div class="flex flex-col gap-4">
<%!-- Multi-day mode toggle --%>
<div class="flex items-center gap-3 p-3 bg-slate-50 dark:bg-slate-900/50 rounded-xl border border-slate-200 dark:border-slate-700">
<div
class="flex items-center gap-3 p-3 bg-slate-50 dark:bg-slate-900/50 rounded-xl border border-slate-200 dark:border-slate-700 cursor-pointer"
phx-click="toggle_multi_day"
phx-target={@myself}
>
<input
type="checkbox"
id={"multi-day-#{@id}"}
phx-change="toggle_multi_day"
phx-target={@myself}
checked={@multi_day_mode}
class="size-4 rounded border-slate-300 dark:border-slate-600 text-primary focus:ring-primary dark:bg-slate-700"
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 cursor-pointer select-none">
<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>

View file

@ -125,10 +125,10 @@
Space
</th>
<th class="px-6 py-3 text-left text-xs font-bold text-slate-600 dark:text-slate-400 uppercase tracking-wider">
Date
Start
</th>
<th class="px-6 py-3 text-left text-xs font-bold text-slate-600 dark:text-slate-400 uppercase tracking-wider">
Time
End
</th>
<th class="px-6 py-3 text-left text-xs font-bold text-slate-600 dark:text-slate-400 uppercase tracking-wider">
Customer
@ -174,14 +174,14 @@
</td>
<td class="px-6 py-4 whitespace-nowrap">
<p class="text-sm text-slate-900 dark:text-slate-200">
{Calendar.strftime(booking.date, "%b %d, %Y")}
{SpazioSolazzo.CalendarExt.format_datetime_range_start(booking.start_datetime)}
</p>
</td>
<td class="px-6 py-4 whitespace-nowrap">
<p class="text-sm text-slate-900 dark:text-slate-200">
{Calendar.strftime(booking.start_time, "%H:%M")} - {Calendar.strftime(
booking.end_time,
"%H:%M"
{SpazioSolazzo.CalendarExt.format_datetime_range_end(
booking.start_datetime,
booking.end_datetime
)}
</p>
</td>
@ -278,10 +278,10 @@
Space
</th>
<th class="px-6 py-3 text-left text-xs font-bold text-slate-600 dark:text-slate-400 uppercase tracking-wider">
Date
Start
</th>
<th class="px-6 py-3 text-left text-xs font-bold text-slate-600 dark:text-slate-400 uppercase tracking-wider">
Time
End
</th>
<th class="px-6 py-3 text-left text-xs font-bold text-slate-600 dark:text-slate-400 uppercase tracking-wider">
Customer
@ -324,14 +324,14 @@
</td>
<td class="px-6 py-4 whitespace-nowrap">
<p class="text-sm text-slate-900 dark:text-slate-200">
{Calendar.strftime(booking.date, "%b %d, %Y")}
{SpazioSolazzo.CalendarExt.format_datetime_range_start(booking.start_datetime)}
</p>
</td>
<td class="px-6 py-4 whitespace-nowrap">
<p class="text-sm text-slate-900 dark:text-slate-200">
{Calendar.strftime(booking.start_time, "%H:%M")} - {Calendar.strftime(
booking.end_time,
"%H:%M"
{SpazioSolazzo.CalendarExt.format_datetime_range_end(
booking.start_datetime,
booking.end_datetime
)}
</p>
</td>

View file

@ -66,20 +66,14 @@ defmodule SpazioSolazzoWeb.Admin.WalkInLive do
end
end
def handle_event("update_customer_name", %{"value" => value}, socket) do
{:noreply, assign(socket, customer_name: value)}
end
def handle_event("update_customer_email", %{"value" => value}, socket) do
{:noreply, assign(socket, customer_email: value)}
end
def handle_event("update_customer_phone", %{"value" => value}, socket) do
{:noreply, assign(socket, customer_phone: value)}
end
def handle_event("update_customer_comment", %{"value" => value}, socket) do
{:noreply, assign(socket, customer_comment: value)}
def handle_event("update_customer_details", params, socket) do
{:noreply,
assign(socket,
customer_name: Map.get(params, "customer_name", ""),
customer_email: Map.get(params, "customer_email", ""),
customer_phone: Map.get(params, "customer_phone", ""),
customer_comment: Map.get(params, "customer_comment", "")
)}
end
def handle_event("create_booking", _, socket) do

View file

@ -204,14 +204,14 @@
</div>
</header>
<div class="space-y-4 max-w-2xl">
<.form for={%{}} phx-change="update_customer_details" class="space-y-4 max-w-2xl">
<div class="relative">
<span class="absolute inset-y-0 left-0 flex items-center pl-3 text-slate-400">
<.icon name="hero-user" class="w-5 h-5" />
</span>
<input
name="customer_name"
value={@customer_name}
phx-change="update_customer_name"
class="w-full bg-slate-50 dark:bg-slate-900 border border-slate-200 dark:border-slate-700 text-slate-900 dark:text-white text-sm rounded-xl pl-10 pr-4 py-3 focus:ring-2 focus:ring-primary focus:border-primary transition-all placeholder:text-slate-400"
placeholder="Customer Name"
type="text"
@ -223,8 +223,8 @@
<.icon name="hero-envelope" class="w-5 h-5" />
</span>
<input
name="customer_email"
value={@customer_email}
phx-change="update_customer_email"
class="w-full bg-slate-50 dark:bg-slate-900 border border-slate-200 dark:border-slate-700 text-slate-900 dark:text-white text-sm rounded-xl pl-10 pr-4 py-3 focus:ring-2 focus:ring-primary focus:border-primary transition-all placeholder:text-slate-400"
placeholder="customer@example.com"
type="email"
@ -236,8 +236,8 @@
<.icon name="hero-phone" class="w-5 h-5" />
</span>
<input
name="customer_phone"
value={@customer_phone}
phx-change="update_customer_phone"
class="w-full bg-slate-50 dark:bg-slate-900 border border-slate-200 dark:border-slate-700 text-slate-900 dark:text-white text-sm rounded-xl pl-10 pr-4 py-3 focus:ring-2 focus:ring-primary focus:border-primary transition-all placeholder:text-slate-400"
placeholder="Customer Phone Number (Optional)"
type="tel"
@ -246,38 +246,25 @@
<div class="relative">
<textarea
name="customer_comment"
value={@customer_comment}
phx-change="update_customer_comment"
rows="3"
class="w-full bg-slate-50 dark:bg-slate-900 border border-slate-200 dark:border-slate-700 text-slate-900 dark:text-white text-sm rounded-xl px-4 py-3 focus:ring-2 focus:ring-primary focus:border-primary transition-all placeholder:text-slate-400"
placeholder="Additional notes or comments (Optional)"
></textarea>
</div>
</div>
</.form>
</article>
<%!-- Submit button --%>
<div class="sticky bottom-6 z-20">
<div class="bg-slate-900 dark:bg-primary rounded-2xl p-4 md:p-6 shadow-2xl border border-slate-800 dark:border-slate-600 flex flex-col sm:flex-row justify-between items-center gap-4 backdrop-blur-xl bg-opacity-95 dark:bg-opacity-95">
<div class="flex items-center gap-4">
<div class="bg-yellow-500/20 p-2 rounded-full hidden sm:block">
<.icon name="hero-currency-dollar" class="w-5 h-5 text-yellow-500" />
</div>
<div>
<p class="text-white font-bold">Ready to book?</p>
<p class="text-xs text-slate-300 dark:text-slate-200">
Payment collected upon arrival.
</p>
</div>
</div>
<button
phx-click="create_booking"
class="w-full sm:w-auto flex items-center justify-center gap-2 overflow-hidden rounded-xl h-11 px-8 bg-yellow-500 hover:bg-yellow-400 transition-colors text-slate-900 text-sm font-bold shadow-lg shadow-yellow-500/20"
>
<span>Confirm Booking</span>
<.icon name="hero-arrow-right" class="w-4 h-4" />
</button>
</div>
<div class="flex justify-center pt-4">
<button
phx-click="create_booking"
class="w-full sm:w-auto flex items-center justify-center gap-2 overflow-hidden rounded-xl h-12 px-10 bg-primary hover:bg-primary/90 transition-colors text-white text-base font-bold shadow-lg shadow-primary/30"
>
<span>Create Booking</span>
<.icon name="hero-check" class="w-5 h-5" />
</button>
</div>
</div>
</section>

View file

@ -70,6 +70,8 @@ defmodule SpazioSolazzo.Repo.Migrations.CreateBaseResources do
create table(:bookings, primary_key: false) do
add :id, :uuid, null: false, default: fragment("gen_random_uuid()"), primary_key: true
add :start_datetime, :utc_datetime, null: false
add :end_datetime, :utc_datetime, null: false
add :date, :date, null: false
add :customer_name, :text, null: false
add :customer_email, :text, null: false

View file

@ -12,6 +12,30 @@
"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",
@ -224,7 +248,7 @@
"custom_indexes": [],
"custom_statements": [],
"has_create_action": true,
"hash": "0EFE49884DF5DC66BF3F1E125132A2F3E18DD66AC732121184A93707260C5225",
"hash": "CBA4F1C24A9B1AB8A2E4EF42917C1DE5F143C74D0D175D1A29156ECCE6FD5660",
"identities": [],
"multitenancy": {
"attribute": null,

View file

@ -13,9 +13,8 @@ defmodule SpazioSolazzo.BookingSystem.Booking.RequestCreatedEmailWorkerTest do
"customer_phone" => "+1234567890",
"customer_comment" => "Test comment",
"space_name" => "Coworking Space",
"date" => "Monday, February 02",
"start_time" => ~T[09:00:00],
"end_time" => ~T[13:00:00]
"start_datetime" => "2026-02-02T09:00:00Z",
"end_datetime" => "2026-02-02T13:00:00Z"
}
assert :ok = perform_job(RequestCreatedEmailWorker, job_args)
@ -35,9 +34,8 @@ defmodule SpazioSolazzo.BookingSystem.Booking.RequestCreatedEmailWorkerTest do
"customer_phone" => "+1234567890",
"customer_comment" => "Test comment",
"space_name" => "Coworking Space",
"date" => "Monday, February 02",
"start_time" => ~T[09:00:00],
"end_time" => ~T[13:00:00]
"start_datetime" => "2026-02-02T09:00:00Z",
"end_datetime" => "2026-02-02T13:00:00Z"
}
admin_email = Application.get_env(:spazio_solazzo, :admin_email)
@ -59,9 +57,8 @@ defmodule SpazioSolazzo.BookingSystem.Booking.RequestCreatedEmailWorkerTest do
"customer_phone" => "+1234567890",
"customer_comment" => "Another test",
"space_name" => "Meeting Room",
"date" => "Tuesday, February 03",
"start_time" => ~T[14:00:00],
"end_time" => ~T[18:00:00]
"start_datetime" => "2026-02-03T14:00:00Z",
"end_datetime" => "2026-02-03T18:00:00Z"
}
admin_email = Application.get_env(:spazio_solazzo, :admin_email)
@ -85,9 +82,8 @@ defmodule SpazioSolazzo.BookingSystem.Booking.RequestCreatedEmailWorkerTest do
"customer_phone" => "+1234567890",
"customer_comment" => "Test",
"space_name" => "Music Room",
"date" => "Wednesday, February 04",
"start_time" => ~T[10:00:00],
"end_time" => ~T[12:00:00]
"start_datetime" => "2026-02-04T10:00:00Z",
"end_datetime" => "2026-02-04T12:00:00Z"
}
assert :ok = perform_job(RequestCreatedEmailWorker, job_args)
@ -110,9 +106,8 @@ defmodule SpazioSolazzo.BookingSystem.Booking.RequestCreatedEmailWorkerTest do
"customer_phone" => "+1234567890",
"customer_comment" => "Admin comment",
"space_name" => "Coworking Space",
"date" => "Thursday, February 05",
"start_time" => ~T[09:00:00],
"end_time" => ~T[11:00:00]
"start_datetime" => "2026-02-05T09:00:00Z",
"end_datetime" => "2026-02-05T11:00:00Z"
}
admin_email = Application.get_env(:spazio_solazzo, :admin_email)

View file

@ -0,0 +1,481 @@
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,
10
)
%{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_public_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

View file

@ -0,0 +1,83 @@
defmodule SpazioSolazzoWeb.Admin.WalkInLiveSimpleTest 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")
# 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
setup do
{:ok, space} =
BookingSystem.create_space(
"Coworking",
"coworking",
"Coworking space",
5,
10
)
user = create_admin_user()
%{space: space, user: user}
end
describe "walk-in booking creation bug" do
test "can create booking by directly setting assigns", %{conn: conn, user: user, space: space} do
conn = log_in_user(conn, user)
{:ok, view, _html} = live(conn, "/admin/walk-in")
tomorrow = Date.add(Date.utc_today(), 1)
# Simulate the calendar component sending the date_selected message
send(view.pid, {:date_selected, tomorrow, tomorrow})
# Give it a moment to process
:timer.sleep(100)
# Fill in customer details using the form
view
|> form("form[phx-change='update_customer_details']", %{
"customer_name" => "John Doe",
"customer_email" => "john@example.com"
})
|> render_change()
# Try to create the booking
html =
view
|> element("button[phx-click='create_booking']")
|> render_click()
# Check if it succeeded or failed
if html =~ "Walk-in booking created successfully" do
IO.puts("✓ SUCCESS: Booking was created")
else
if html =~ "Please fill in all required fields and select a date" do
IO.puts("✗ BUG FOUND: Error message shown even though all fields are filled!")
IO.puts("\nThis is the bug the user is experiencing.")
else
IO.puts("? Unexpected result")
end
end
# Verify booking was created
{:ok, bookings} = BookingSystem.list_accepted_space_bookings_by_date(space.id, tomorrow)
if length(bookings) > 0 do
assert true
else
flunk("Booking was not created even though all requirements were met")
end
end
end
end

View file

@ -0,0 +1,245 @@
defmodule SpazioSolazzoWeb.Admin.WalkInLiveTest 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
setup do
{:ok, space} =
BookingSystem.create_space(
"Coworking",
"coworking",
"Coworking space",
5,
10
)
user = create_admin_user()
%{space: space, user: user}
end
describe "walk-in booking form" do
test "displays the form with calendar and customer details", %{conn: conn, user: user} do
conn = log_in_user(conn, user)
{:ok, view, _html} = live(conn, "/admin/walk-in")
assert has_element?(view, "form[phx-change='update_customer_details']")
assert has_element?(view, "input[name='customer_name']")
assert has_element?(view, "input[name='customer_email']")
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
conn = log_in_user(conn, user)
{:ok, view, _html} = live(conn, "/admin/walk-in")
tomorrow = Date.add(Date.utc_today(), 1)
# Simulate date selection by sending message to LiveView
send(view.pid, {:date_selected, tomorrow, tomorrow})
:timer.sleep(50)
# Fill in customer details
view
|> form("form[phx-change='update_customer_details']", %{
"customer_name" => "John Doe",
"customer_email" => "john@example.com"
})
|> render_change()
# Submit the form
html =
view
|> element("button[phx-click='create_booking']")
|> render_click()
assert html =~ "Walk-in booking created successfully"
# Verify booking was created
{:ok, bookings} = BookingSystem.list_accepted_space_bookings_by_date(space.id, tomorrow)
assert length(bookings) == 1
booking = hd(bookings)
assert booking.customer_name == "John Doe"
assert booking.customer_email == "john@example.com"
assert booking.state == :accepted
end
test "shows error when no date is selected", %{conn: conn, user: user} do
conn = log_in_user(conn, user)
{:ok, view, _html} = live(conn, "/admin/walk-in")
# Fill in customer details without selecting a date
view
|> form("form[phx-change='update_customer_details']", %{
"customer_name" => "John Doe",
"customer_email" => "john@example.com"
})
|> render_change()
# Try to submit
html =
view
|> element("button[phx-click='create_booking']")
|> render_click()
assert html =~ "Please fill in all required fields and select a date"
end
test "shows error when customer name is missing", %{conn: conn, user: user} do
conn = log_in_user(conn, user)
{:ok, view, _html} = live(conn, "/admin/walk-in")
tomorrow = Date.add(Date.utc_today(), 1)
send(view.pid, {:date_selected, tomorrow, tomorrow})
:timer.sleep(50)
view
|> form("form[phx-change='update_customer_details']", %{
"customer_email" => "john@example.com"
})
|> render_change()
html =
view
|> element("button[phx-click='create_booking']")
|> render_click()
assert html =~ "Please fill in all required fields and select a date"
end
test "shows error when customer email is missing", %{conn: conn, user: user} do
conn = log_in_user(conn, user)
{:ok, view, _html} = live(conn, "/admin/walk-in")
tomorrow = Date.add(Date.utc_today(), 1)
send(view.pid, {:date_selected, tomorrow, tomorrow})
:timer.sleep(50)
view
|> form("form[phx-change='update_customer_details']", %{
"customer_name" => "John Doe"
})
|> render_change()
html =
view
|> element("button[phx-click='create_booking']")
|> render_click()
assert html =~ "Please fill in all required fields and select a date"
end
test "creates multi-day walk-in booking", %{conn: conn, user: user, space: space} do
conn = log_in_user(conn, user)
{:ok, view, _html} = live(conn, "/admin/walk-in")
# Select date range (3 days)
start_date = Date.add(Date.utc_today(), 1)
end_date = Date.add(Date.utc_today(), 3)
send(view.pid, {:date_selected, start_date, end_date})
:timer.sleep(50)
# Fill in customer details
view
|> form("form[phx-change='update_customer_details']", %{
"customer_name" => "Jane Smith",
"customer_email" => "jane@example.com"
})
|> render_change()
# Submit the form
html =
view
|> element("button[phx-click='create_booking']")
|> render_click()
assert html =~ "Walk-in booking created successfully"
# Verify booking was created and spans multiple days
{:ok, bookings} = BookingSystem.list_accepted_space_bookings_by_date(space.id, start_date)
assert length(bookings) == 1
booking = hd(bookings)
assert booking.customer_name == "Jane Smith"
# Verify booking appears on all days in the range
{:ok, day2_bookings} =
BookingSystem.list_accepted_space_bookings_by_date(space.id, Date.add(start_date, 1))
assert length(day2_bookings) == 1
{:ok, day3_bookings} = BookingSystem.list_accepted_space_bookings_by_date(space.id, end_date)
assert length(day3_bookings) == 1
end
test "includes optional phone and comment", %{conn: conn, user: user, space: space} do
conn = log_in_user(conn, user)
{:ok, view, _html} = live(conn, "/admin/walk-in")
tomorrow = Date.add(Date.utc_today(), 1)
send(view.pid, {:date_selected, tomorrow, tomorrow})
:timer.sleep(50)
view
|> form("form[phx-change='update_customer_details']", %{
"customer_name" => "John Doe",
"customer_email" => "john@example.com",
"customer_phone" => "+39 1234567890",
"customer_comment" => "Special request"
})
|> render_change()
html =
view
|> element("button[phx-click='create_booking']")
|> render_click()
assert html =~ "Walk-in booking created successfully"
{:ok, bookings} = BookingSystem.list_accepted_space_bookings_by_date(space.id, tomorrow)
booking = hd(bookings)
assert booking.customer_phone == "+39 1234567890"
assert booking.customer_comment == "Special request"
end
test "clears form after successful booking", %{conn: conn, user: user} do
conn = log_in_user(conn, user)
{:ok, view, _html} = live(conn, "/admin/walk-in")
tomorrow = Date.add(Date.utc_today(), 1)
send(view.pid, {:date_selected, tomorrow, tomorrow})
:timer.sleep(50)
view
|> form("form[phx-change='update_customer_details']", %{
"customer_name" => "John Doe",
"customer_email" => "john@example.com"
})
|> render_change()
html =
view
|> element("button[phx-click='create_booking']")
|> render_click()
assert html =~ "Walk-in booking created successfully"
# Check that form inputs are cleared by verifying empty values
html = render(view)
assert html =~ "Not selected"
assert html =~ ~s(value="")
end
end
end