refactor: booking actions to simplify them

This commit is contained in:
JasterV 2026-02-02 19:45:40 +01:00
parent bc0ff021e8
commit b4a524605a
14 changed files with 684 additions and 363 deletions

View file

@ -26,19 +26,13 @@ defmodule SpazioSolazzo.BookingSystem do
end
resource SpazioSolazzo.BookingSystem.Booking do
define :list_accepted_space_bookings_by_date,
action: :list_accepted_space_bookings_by_date,
args: [:space_id, :date]
define :list_booking_requests,
action: :list_booking_requests,
define :admin_search_bookings,
action: :admin_dashboard_search,
args: [:space_id, :email, :date]
define :count_pending_requests, action: :count_pending_requests
define :list_bookings_by_datetime_range,
action: :by_datetime_range_and_status,
args: [:space_id, :user_id, :start_datetime, :end_datetime, :states]
define :search_bookings,
action: :search,
args: [:space_id, :start_datetime, :end_datetime, :states, :select]
define :create_booking,
action: :create,

View file

@ -54,25 +54,52 @@ defmodule SpazioSolazzo.BookingSystem.Booking do
actions do
defaults [:read]
read :list_accepted_space_bookings_by_date do
argument :space_id, :uuid, allow_nil?: false
argument :date, :date, allow_nil?: false
read :search do
description "Fetch bookings within a date/time range with optional filters"
argument :space_id, :uuid, allow_nil?: true
argument :start_datetime, :datetime, allow_nil?: false
argument :end_datetime, :datetime, allow_nil?: false
argument :states, {:array, :atom}, allow_nil?: true
argument :select, {:array, :atom}, allow_nil?: true
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")
start_dt = Ash.Query.get_argument(query, :start_datetime)
end_dt = Ash.Query.get_argument(query, :end_datetime)
query
|> Ash.Query.filter(
start_datetime < ^day_end and end_datetime > ^day_start and state == :accepted
)
# Base datetime overlap filter
query =
Ash.Query.filter(
query,
start_datetime < ^end_dt and end_datetime > ^start_dt
)
# Optional space filter
query =
case Ash.Query.get_argument(query, :space_id) do
nil -> query
space_id -> Ash.Query.filter(query, space_id == ^space_id)
end
# Optional states filter
query =
case Ash.Query.get_argument(query, :states) do
nil -> query
[] -> query
states -> Ash.Query.filter(query, state in ^states)
end
case Ash.Query.get_argument(query, :select) do
nil -> query
[] -> query
select -> Ash.Query.select(query, select)
end
end
filter expr(space_id == ^arg(:space_id))
end
read :list_booking_requests do
read :admin_dashboard_search do
description "Search query tailored for the admin booking management panel"
argument :space_id, :uuid, allow_nil?: true
argument :email, :string, allow_nil?: true
argument :date, :date, allow_nil?: true
@ -105,53 +132,6 @@ defmodule SpazioSolazzo.BookingSystem.Booking do
end
end
read :count_pending_requests do
filter expr(state == :requested)
end
read :by_datetime_range_and_status do
description "Fetch bookings within a date/time range with optional filters"
argument :space_id, :uuid, allow_nil?: true
argument :user_id, :uuid, allow_nil?: true
argument :start_datetime, :datetime, allow_nil?: false
argument :end_datetime, :datetime, allow_nil?: false
argument :states, {:array, :atom}, allow_nil?: true
prepare fn query, _ctx ->
start_dt = Ash.Query.get_argument(query, :start_datetime)
end_dt = Ash.Query.get_argument(query, :end_datetime)
# Base datetime overlap filter
query =
Ash.Query.filter(
query,
start_datetime < ^end_dt and end_datetime > ^start_dt
)
# Optional space filter
query =
case Ash.Query.get_argument(query, :space_id) do
nil -> query
space_id -> Ash.Query.filter(query, space_id == ^space_id)
end
# Optional user filter
query =
case Ash.Query.get_argument(query, :user_id) do
nil -> query
user_id -> Ash.Query.filter(query, user_id == ^user_id)
end
# Optional states filter
case Ash.Query.get_argument(query, :states) do
nil -> query
[] -> query
states -> Ash.Query.filter(query, state in ^states)
end
end
end
create :create do
argument :space_id, :uuid, allow_nil?: false
argument :user_id, :uuid, allow_nil?: true

View file

@ -23,12 +23,12 @@ defmodule SpazioSolazzo.BookingSystem.TimeSlotTemplate.Calculations.SlotBookingS
day_end = DateTime.new!(date, ~T[23:59:59], "Etc/UTC")
{:ok, all_bookings} =
SpazioSolazzo.BookingSystem.list_bookings_by_datetime_range(
SpazioSolazzo.BookingSystem.search_bookings(
space_id,
nil,
day_start,
day_end,
[:requested, :accepted]
[:requested, :accepted],
[:start_datetime, :end_datetime, :state, :user_id]
)
# Calculate stats for each slot using the cached bookings

View file

@ -82,21 +82,14 @@ defmodule SpazioSolazzoWeb.Admin.AdminCalendarComponent do
if Date.compare(date, Date.utc_today()) == :lt do
{:noreply, socket}
else
# Check capacity
capacity_status = Map.get(socket.assigns.day_capacities, date, :available)
socket =
if socket.assigns.multi_day_mode do
handle_multi_day_selection(socket, date)
else
handle_single_day_selection(socket, date)
end
if capacity_status == :over_capacity do
{:noreply, socket}
else
socket =
if socket.assigns.multi_day_mode do
handle_multi_day_selection(socket, date)
else
handle_single_day_selection(socket, date)
end
{:noreply, socket}
end
{:noreply, socket}
end
_ ->
@ -147,53 +140,58 @@ defmodule SpazioSolazzoWeb.Admin.AdminCalendarComponent do
space_id = socket.assigns.space_id
current_month = socket.assigns.current_month
# Get all days in the current month
first_day = Date.beginning_of_month(current_month)
last_day = Date.end_of_month(current_month)
start_of_month = Date.beginning_of_month(current_month)
end_of_month = Date.end_of_month(current_month)
# Calculate capacity for each day
day_capacities =
first_day
|> Date.range(last_day)
|> Enum.map(fn date ->
capacity = get_day_capacity(space_id, date)
{date, capacity}
end)
|> Map.new()
# Single query for entire month
{:ok, bookings} =
BookingSystem.search_bookings(
space_id,
DateTime.new!(start_of_month, ~T[00:00:00]),
DateTime.new!(end_of_month, ~T[23:59:59]),
[:accepted],
[:start_datetime, :end_datetime]
)
# Count bookings per day
day_data = compute_day_data(bookings, start_of_month, end_of_month)
# Build calendar grid
calendar_weeks = build_calendar_grid(first_day, last_day)
calendar_weeks = build_calendar_grid(start_of_month, end_of_month)
assign(socket,
day_capacities: day_capacities,
day_data: day_data,
calendar_weeks: calendar_weeks,
month_name: Calendar.strftime(current_month, "%B %Y")
)
end
defp get_day_capacity(space_id, date) do
# Get the space to check capacities
case Ash.get(SpazioSolazzo.BookingSystem.Space, space_id) do
{:ok, space} ->
# Get all bookings for this day
case BookingSystem.list_accepted_space_bookings_by_date(space_id, date) do
{:ok, bookings} ->
# Count unique booking slots (simplified - counts all bookings)
booking_count = length(bookings)
defp compute_day_data(bookings, start_date, end_date) do
# Initialize all days with zero count
date_range = Date.range(start_date, end_date)
if booking_count >= space.capacity do
:over_capacity
else
:available
end
initial_map =
Enum.reduce(date_range, %{}, fn date, acc ->
Map.put(acc, date, 0)
end)
_ ->
:available
# Count bookings for each day
Enum.reduce(bookings, initial_map, fn booking, acc ->
# Get all dates this booking spans
booking_start_date = DateTime.to_date(booking.start_datetime)
booking_end_date = DateTime.to_date(booking.end_datetime)
booking_dates = Date.range(booking_start_date, booking_end_date)
# Increment count for each day this booking touches
Enum.reduce(booking_dates, acc, fn date, inner_acc ->
case Map.get(inner_acc, date) do
# Date outside month range
nil -> inner_acc
count -> Map.put(inner_acc, date, count + 1)
end
_ ->
:available
end
end)
end)
end
defp build_calendar_grid(first_day, last_day) do
@ -244,25 +242,17 @@ defmodule SpazioSolazzoWeb.Admin.AdminCalendarComponent do
defp is_end_date?(date, _, end_date), do: Date.compare(date, end_date) == :eq
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, 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"
base = "relative aspect-square flex flex-col items-start justify-start p-2 transition-all"
cond do
is_past ->
[base, "text-slate-400 dark:text-slate-600 cursor-not-allowed opacity-50"]
capacity == :over_capacity ->
[
base,
"bg-orange-100 dark:bg-orange-900/20 text-slate-400 dark:text-slate-500 border border-orange-300 dark:border-orange-800/30 cursor-not-allowed"
]
in_range && assigns.multi_day_mode && assigns.end_date != nil ->
cond do
is_start ->
@ -293,16 +283,8 @@ defmodule SpazioSolazzoWeb.Admin.AdminCalendarComponent do
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"
"rounded-lg bg-white dark:bg-slate-800 hover:bg-slate-50 dark:hover:bg-slate-700 text-slate-700 dark:text-slate-200 border border-slate-200 dark:border-slate-600 hover:border-primary dark:hover:border-primary"
]
end
end
defp capacity_indicator_color(capacity) do
case capacity do
:available -> "bg-green-500"
:over_capacity -> "bg-orange-500"
_ -> "bg-slate-300"
end
end
end

View file

@ -23,24 +23,8 @@
<%!-- Legend --%>
<div class="flex items-center justify-between mb-2">
<h3 class="text-sm font-bold uppercase tracking-wider text-slate-500 dark:text-slate-400">
Availability Calendar
Booking Calendar
</h3>
<div class="flex items-center gap-2 text-xs font-medium">
<div class="flex items-center gap-1">
<div class="w-2 h-2 rounded-full bg-green-500"></div>
<span class="text-slate-600 dark:text-slate-400">Available</span>
</div>
<div class="flex items-center gap-1">
<div class="w-2 h-2 rounded-full bg-orange-500"></div>
<span class="text-slate-600 dark:text-slate-400">Overbooked</span>
</div>
<%= if @multi_day_mode && @start_date do %>
<div class="flex items-center gap-1">
<div class="w-2 h-2 rounded-full bg-primary"></div>
<span class="text-slate-600 dark:text-slate-400">Selected</span>
</div>
<% end %>
</div>
</div>
<%!-- Calendar --%>
@ -84,84 +68,70 @@
<%= if day == nil do %>
<div class="aspect-square"></div>
<% else %>
<% capacity = Map.get(@day_capacities, day, :available) %>
<% count = Map.get(@day_data, day, 0) %>
<% is_past = Date.compare(day, Date.utc_today()) == :lt %>
<% is_disabled = is_past || capacity == :over_capacity %>
<% is_start = is_start_date?(day, @start_date, @end_date) %>
<% is_end = is_end_date?(day, @start_date, @end_date) %>
<button
phx-click={if !is_disabled, do: "select_date", else: nil}
phx-click={if !is_past, do: "select_date", else: nil}
phx-value-date={Date.to_iso8601(day)}
phx-target={@myself}
class={day_classes(day, assigns)}
disabled={is_disabled}
title={
cond do
is_past -> "Past date"
capacity == :over_capacity -> "Overbooked"
true -> "Select date"
end
}
disabled={is_past}
title={if is_past, do: "Past date", else: "Select date"}
>
<span class={[
"text-sm",
cond do
is_start || is_end -> "font-bold"
true -> "font-medium"
end
]}>
{day.day}
</span>
<%= cond do %>
<% is_start && @multi_day_mode && @end_date != nil -> %>
<span class="text-[10px] uppercase font-bold tracking-tighter mt-0.5">
Start
</span>
<% is_end && @multi_day_mode && @end_date != nil -> %>
<span class="text-[10px] uppercase font-bold tracking-tighter mt-0.5">
End
</span>
<% !is_past && !is_start && !is_end -> %>
<div class={[
"h-1 w-1 rounded-full mt-1",
capacity_indicator_color(capacity)
]}>
</div>
<% true -> %>
<div class="h-1 mt-1"></div>
<%!-- Booking count badge - floating in top right corner --%>
<%= if count > 0 && !is_past do %>
<.link
navigate={~p"/admin/bookings?date=#{Date.to_string(day)}"}
class="absolute -top-2 -right-2 z-20 flex items-center gap-1 px-2 py-1 rounded-full bg-info text-info-content shadow-lg hover:shadow-xl hover:scale-110 transition-all duration-200 ring-2 ring-white dark:ring-slate-800"
title={"View #{count} booking#{if count > 1, do: "s", else: ""}"}
>
<.icon name="hero-calendar-days" class="w-3 h-3" />
<span class="text-xs font-bold">{count}</span>
</.link>
<% end %>
<div class="flex flex-col items-start justify-start w-full h-full">
<span class={[
"text-xs",
cond do
is_start || is_end -> "font-bold"
true -> "font-medium"
end
]}>
{day.day}
</span>
<%= cond do %>
<% is_start && @multi_day_mode && @end_date != nil -> %>
<span class="text-[10px] uppercase font-bold tracking-tighter mt-auto">
Start
</span>
<% is_end && @multi_day_mode && @end_date != nil -> %>
<span class="text-[10px] uppercase font-bold tracking-tighter mt-auto">
End
</span>
<% true -> %>
<div class="flex-1"></div>
<% end %>
</div>
</button>
<% end %>
<% end %>
<% end %>
</div>
<%!-- Legend explaining booking counts --%>
<div class="mt-4 p-3 bg-base-200 rounded-lg">
<p class="text-sm text-base-content flex items-center gap-2">
<span class="flex items-center gap-1 px-2 py-1 rounded-full bg-info text-info-content shadow-md">
<.icon name="hero-calendar-days" class="w-3 h-3" />
<span class="text-xs font-bold">#</span>
</span>
<span>Click badge to view bookings for that day</span>
</p>
</div>
</div>
<%!-- Warning for capacity issues in multi-day range --%>
<%= if @multi_day_mode && @start_date && @end_date do %>
<% has_full_days =
@start_date
|> Date.range(@end_date)
|> Enum.any?(fn date ->
Map.get(@day_capacities, date, :available) == :over_capacity
end) %>
<%= if has_full_days do %>
<div class="rounded-xl bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-900/50 p-4 flex gap-3 items-start animate-pulse">
<div class="shrink-0 text-red-600 dark:text-red-400 mt-0.5">
<.icon name="hero-exclamation-triangle" class="w-5 h-5" />
</div>
<div>
<h4 class="text-sm font-bold text-red-900 dark:text-red-200">
Attention
</h4>
<p class="text-xs font-medium text-red-700 dark:text-red-300 mt-1">
Some days in your selected range are overbooked.
</p>
</div>
</div>
<% end %>
<% end %>
</div>

View file

@ -9,7 +9,7 @@ defmodule SpazioSolazzoWeb.Admin.BookingManagementLive do
def mount(_params, _session, socket) do
{:ok, spaces} = Ash.read(SpazioSolazzo.BookingSystem.Space)
{:ok, bookings} = BookingSystem.list_booking_requests(nil, nil, nil, load: [:space, :user])
{:ok, bookings} = BookingSystem.admin_search_bookings(nil, nil, nil, load: [:space, :user])
# Separate pending and other bookings
{pending, past} = Enum.split_with(bookings, &(&1.state == :requested))
@ -36,6 +36,34 @@ defmodule SpazioSolazzoWeb.Admin.BookingManagementLive do
)}
end
def handle_params(params, _uri, socket) do
# Parse date from URL if present
filter_date =
case Map.get(params, "date") do
nil ->
nil
"" ->
nil
date_string ->
case Date.from_iso8601(date_string) do
{:ok, date} -> date
{:error, _} -> nil
end
end
# If we have a date from URL, apply it to filters
socket =
if filter_date && socket.assigns.filter_date != filter_date do
apply_date_filter(socket, filter_date)
else
socket
end
{:noreply, socket}
end
def handle_event("toggle_expand", %{"booking_id" => booking_id}, socket) do
expanded =
if MapSet.member?(socket.assigns.expanded_booking_ids, booking_id) do
@ -57,7 +85,7 @@ defmodule SpazioSolazzoWeb.Admin.BookingManagementLive do
else: Date.from_iso8601!(params["date"])
{:ok, bookings} =
BookingSystem.list_booking_requests(space_id, email, date, load: [:space, :user])
BookingSystem.admin_search_bookings(space_id, email, date, load: [:space, :user])
{pending, past} = Enum.split_with(bookings, &(&1.state == :requested))
@ -72,7 +100,7 @@ defmodule SpazioSolazzoWeb.Admin.BookingManagementLive do
end
def handle_event("clear_filters", _, socket) do
{:ok, bookings} = BookingSystem.list_booking_requests(nil, nil, nil, load: [:space, :user])
{:ok, bookings} = BookingSystem.admin_search_bookings(nil, nil, nil, load: [:space, :user])
{pending, past} = Enum.split_with(bookings, &(&1.state == :requested))
{:noreply,
@ -169,7 +197,7 @@ defmodule SpazioSolazzoWeb.Admin.BookingManagementLive do
defp refresh_bookings(socket) do
{:ok, bookings} =
BookingSystem.list_booking_requests(
BookingSystem.admin_search_bookings(
socket.assigns.filter_space_id,
socket.assigns.filter_email,
socket.assigns.filter_date,
@ -185,6 +213,22 @@ defmodule SpazioSolazzoWeb.Admin.BookingManagementLive do
)}
end
defp apply_date_filter(socket, date) do
space_id = socket.assigns.filter_space_id
email = if socket.assigns.filter_email == "", do: nil, else: socket.assigns.filter_email
{:ok, bookings} =
BookingSystem.admin_search_bookings(space_id, email, date, load: [:space, :user])
{pending, past} = Enum.split_with(bookings, &(&1.state == :requested))
assign(socket,
pending_bookings: pending,
past_bookings: past,
filter_date: date
)
end
defp status_badge_classes(:requested) do
"bg-amber-100 dark:bg-amber-900/40 text-amber-800 dark:text-amber-200"
end

View file

@ -5,12 +5,12 @@ defmodule SpazioSolazzoWeb.Admin.DashboardLive do
use SpazioSolazzoWeb, :live_view
alias SpazioSolazzo.BookingSystem
def mount(_params, _session, socket) do
# Get pending requests count for the badge
{:ok, pending_requests} = BookingSystem.count_pending_requests()
pending_count = length(pending_requests)
# Get pending requests count directly from database (no data loaded)
{:ok, pending_count} =
Ash.count(SpazioSolazzo.BookingSystem.Booking,
query: [filter: [state: :requested]]
)
{:ok,
assign(socket,

View file

@ -4,47 +4,30 @@ defmodule SpazioSolazzoWeb.Admin.WalkInLive do
"""
use SpazioSolazzoWeb, :live_view
alias SpazioSolazzo.BookingSystem
def mount(_params, _session, socket) do
# Get coworking space
{:ok, spaces} = Ash.read(SpazioSolazzo.BookingSystem.Space)
coworking_space = Enum.find(spaces, &(&1.slug == "coworking"))
{:ok, coworking_space} = BookingSystem.get_space_by_slug("coworking")
if coworking_space == nil do
{:ok,
socket
|> put_flash(:error, "Coworking space not found")
|> push_navigate(to: "/admin/dashboard")}
else
{:ok,
assign(socket,
coworking_space: coworking_space,
multi_day_mode: false,
start_date: nil,
end_date: nil,
selected_date: nil,
start_time: ~T[09:00:00],
end_time: ~T[18:00:00],
customer_name: "",
customer_email: "",
customer_phone: "",
customer_comment: "",
time_slot_warning: nil
)}
end
{:ok,
assign(socket,
coworking_space: coworking_space,
multi_day_mode: false,
start_date: nil,
end_date: nil,
start_time: ~T[09:00:00],
end_time: ~T[18:00:00],
customer_name: "",
customer_email: "",
customer_phone: "",
customer_comment: ""
)}
end
def handle_event("update_start_time", %{"value" => time_str}, socket) do
case Time.from_iso8601(time_str <> ":00") do
{:ok, time} ->
socket =
socket
|> assign(start_time: time)
|> check_time_slot_capacity()
{:noreply, socket}
{:noreply, assign(socket, start_time: time)}
_ ->
{:noreply, socket}
@ -54,12 +37,7 @@ defmodule SpazioSolazzoWeb.Admin.WalkInLive do
def handle_event("update_end_time", %{"value" => time_str}, socket) do
case Time.from_iso8601(time_str <> ":00") do
{:ok, time} ->
socket =
socket
|> assign(end_time: time)
|> check_time_slot_capacity()
{:noreply, socket}
{:noreply, assign(socket, end_time: time)}
_ ->
{:noreply, socket}
@ -106,9 +84,7 @@ defmodule SpazioSolazzoWeb.Admin.WalkInLive do
customer_phone: "",
customer_comment: "",
start_date: nil,
end_date: nil,
selected_date: nil,
time_slot_warning: nil
end_date: nil
)
|> put_flash(:info, "Walk-in booking created successfully")}
@ -127,12 +103,7 @@ defmodule SpazioSolazzoWeb.Admin.WalkInLive do
end
def handle_info({:date_selected, start_date, end_date}, socket) do
socket =
socket
|> assign(start_date: start_date, end_date: end_date, selected_date: nil)
|> check_time_slot_capacity()
{:noreply, socket}
{:noreply, assign(socket, start_date: start_date, end_date: end_date)}
end
def handle_info(_msg, socket) do
@ -140,56 +111,20 @@ defmodule SpazioSolazzoWeb.Admin.WalkInLive do
end
defp get_start_date(socket) do
socket.assigns.start_date || socket.assigns.selected_date
socket.assigns.start_date
end
defp get_end_date(socket) do
socket.assigns.end_date || socket.assigns.selected_date
socket.assigns.end_date
end
defp check_time_slot_capacity(socket) do
# Only check for single-day bookings
if socket.assigns.multi_day_mode || socket.assigns.selected_date == nil do
assign(socket, time_slot_warning: nil)
else
date = socket.assigns.selected_date
start_time = socket.assigns.start_time
end_time = socket.assigns.end_time
space_id = socket.assigns.coworking_space.id
capacity = socket.assigns.coworking_space.capacity
defp days_selected(nil, nil), do: 0
defp days_selected(start_date, nil) when not is_nil(start_date), do: 1
start_datetime = DateTime.new!(date, start_time, "Etc/UTC")
end_datetime = DateTime.new!(date, end_time, "Etc/UTC")
{:ok, bookings} =
BookingSystem.list_bookings_by_datetime_range(
space_id,
nil,
start_datetime,
end_datetime,
[:accepted]
)
accepted_count = length(bookings)
if accepted_count >= capacity do
assign(socket,
time_slot_warning: "This time slot is currently overbooked. Proceed with caution."
)
else
assign(socket, time_slot_warning: nil)
end
end
end
defp days_selected(nil, nil, nil), do: 0
defp days_selected(selected, nil, nil) when not is_nil(selected), do: 1
defp days_selected(nil, start_date, nil) when not is_nil(start_date), do: 1
defp days_selected(nil, start_date, end_date)
defp days_selected(start_date, end_date)
when not is_nil(start_date) and not is_nil(end_date) do
Date.diff(end_date, start_date) + 1
end
defp days_selected(_, _, _), do: 0
defp days_selected(_, _), do: 0
end

View file

@ -65,8 +65,8 @@
Start Date
</span>
<span class="text-sm font-bold text-slate-900 dark:text-white">
<%= if @start_date || @selected_date do %>
{Calendar.strftime(@start_date || @selected_date, "%b %d, %Y")}
<%= if @start_date do %>
{Calendar.strftime(@start_date, "%b %d, %Y")}
<% else %>
<span class="text-slate-400">Not selected</span>
<% end %>
@ -78,8 +78,8 @@
End Date
</span>
<span class="text-sm font-bold text-slate-900 dark:text-white">
<%= if @end_date || @selected_date do %>
{Calendar.strftime(@end_date || @selected_date, "%b %d, %Y")}
<%= if @end_date do %>
{Calendar.strftime(@end_date, "%b %d, %Y")}
<% else %>
<span class="text-slate-400">Not selected</span>
<% end %>
@ -96,7 +96,7 @@
/>
<span>
<%= if @multi_day_mode && @start_date && @end_date do %>
{days_selected(@selected_date, @start_date, @end_date)} Days total
{days_selected(@start_date, @end_date)} Days total
<% else %>
Single Day
<% end %>
@ -168,19 +168,6 @@
/>
</div>
</div>
<%= if @time_slot_warning do %>
<div class="mt-2 p-3 rounded-lg bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800/50 flex items-start gap-3">
<span class="text-red-600 dark:text-red-400 shrink-0">
<.icon name="hero-exclamation-triangle" class="w-5 h-5" />
</span>
<div>
<p class="text-sm font-bold text-red-900 dark:text-red-100 leading-tight">
{@time_slot_warning}
</p>
</div>
</div>
<% end %>
</div>
<% end %>
</div>

View file

@ -0,0 +1,333 @@
defmodule SpazioSolazzo.BookingSystem.BookingMonthCountTest do
use SpazioSolazzo.DataCase
alias SpazioSolazzo.BookingSystem
describe "list_bookings_for_month_count/3" do
setup do
# Create space
{:ok, space} = BookingSystem.create_space("Coworking", "coworking", "Desc", 10)
# Use dates in the future (next month, day 15)
today = Date.utc_today()
next_month = Date.add(today, 30)
test_date = %{next_month | day: 15}
start_datetime = DateTime.new!(test_date, ~T[09:00:00], "Etc/UTC")
end_datetime = DateTime.new!(test_date, ~T[17:00:00], "Etc/UTC")
# Create booking with full data
{:ok, booking} =
BookingSystem.create_walk_in(
space.id,
start_datetime,
end_datetime,
"John Doe",
"john@example.com",
"555-1234",
"Test comment"
)
# Reload booking
{:ok, booking} = Ash.reload(booking)
# Calculate month boundaries
start_of_month = Date.beginning_of_month(test_date)
end_of_month = Date.end_of_month(test_date)
%{
space: space,
booking: booking,
test_date: test_date,
start_of_month: start_of_month,
end_of_month: end_of_month
}
end
test "returns only datetime fields, not full booking data",
%{space: space, start_of_month: start_date, end_of_month: end_date} do
start_datetime = DateTime.new!(start_date, ~T[00:00:00], "Etc/UTC")
end_datetime = DateTime.new!(Date.add(end_date, 1), ~T[00:00:00], "Etc/UTC")
{:ok, bookings} =
BookingSystem.search_bookings(
space.id,
start_datetime,
end_datetime,
[:accepted],
[:start_datetime, :end_datetime]
)
assert length(bookings) == 1
booking = hd(bookings)
# Assert datetime fields ARE present
assert %DateTime{} = booking.start_datetime
assert %DateTime{} = booking.end_datetime
# Assert other fields are NOT loaded
refute Ash.Resource.loaded?(booking, :customer_name)
refute Ash.Resource.loaded?(booking, :customer_email)
refute Ash.Resource.loaded?(booking, :customer_phone)
refute Ash.Resource.loaded?(booking, :customer_comment)
refute Ash.Resource.loaded?(booking, :user)
refute Ash.Resource.loaded?(booking, :space)
end
test "returns empty list for month with no bookings", %{space: space} do
# Query a different month (two months from now)
today = Date.utc_today()
future_month = Date.add(today, 60)
start_date = Date.beginning_of_month(future_month)
end_date = Date.end_of_month(future_month)
start_datetime = DateTime.new!(start_date, ~T[00:00:00], "Etc/UTC")
end_datetime = DateTime.new!(Date.add(end_date, 1), ~T[00:00:00], "Etc/UTC")
{:ok, bookings} =
BookingSystem.search_bookings(
space.id,
start_datetime,
end_datetime,
[:accepted],
[:start_datetime, :end_datetime]
)
assert bookings == []
end
test "handles bookings that span multiple days",
%{space: space, test_date: test_date, start_of_month: start_date, end_of_month: end_date} do
# Create a 3-day booking (day 20-22)
multi_day_start = %{test_date | day: 20}
multi_day_end = %{test_date | day: 22}
start_datetime = DateTime.new!(multi_day_start, ~T[09:00:00], "Etc/UTC")
end_datetime = DateTime.new!(multi_day_end, ~T[17:00:00], "Etc/UTC")
{:ok, _booking} =
BookingSystem.create_walk_in(
space.id,
start_datetime,
end_datetime,
"Jane Doe",
"jane@example.com",
nil,
nil
)
month_start_datetime = DateTime.new!(start_date, ~T[00:00:00], "Etc/UTC")
month_end_datetime = DateTime.new!(Date.add(end_date, 1), ~T[00:00:00], "Etc/UTC")
{:ok, bookings} =
BookingSystem.search_bookings(
space.id,
month_start_datetime,
month_end_datetime,
[:accepted],
[:start_datetime, :end_datetime]
)
# Should have 2 bookings (original + multi-day)
assert length(bookings) == 2
# All should only have datetime fields
Enum.each(bookings, fn booking ->
refute Ash.Resource.loaded?(booking, :customer_name)
refute Ash.Resource.loaded?(booking, :customer_email)
end)
end
test "handles month boundaries correctly",
%{space: space, test_date: test_date, start_of_month: start_date, end_of_month: end_date} do
# Booking starts before month, ends during month (last day of previous month to day 2)
before_month = Date.add(start_date, -1)
during_month = %{test_date | day: 2}
start_datetime1 = DateTime.new!(before_month, ~T[09:00:00], "Etc/UTC")
end_datetime1 = DateTime.new!(during_month, ~T[17:00:00], "Etc/UTC")
{:ok, _before} =
BookingSystem.create_walk_in(
space.id,
start_datetime1,
end_datetime1,
"Before Month",
"before@example.com",
nil,
nil
)
# Booking starts during month, ends after month (day 27 to first day of next month)
during_month2 = %{test_date | day: min(27, Date.days_in_month(test_date))}
after_month = Date.add(end_date, 1)
start_datetime2 = DateTime.new!(during_month2, ~T[09:00:00], "Etc/UTC")
end_datetime2 = DateTime.new!(after_month, ~T[17:00:00], "Etc/UTC")
{:ok, _after} =
BookingSystem.create_walk_in(
space.id,
start_datetime2,
end_datetime2,
"After Month",
"after@example.com",
nil,
nil
)
month_start_datetime = DateTime.new!(start_date, ~T[00:00:00], "Etc/UTC")
month_end_datetime = DateTime.new!(Date.add(end_date, 1), ~T[00:00:00], "Etc/UTC")
{:ok, bookings} =
BookingSystem.search_bookings(
space.id,
month_start_datetime,
month_end_datetime,
[:accepted],
[:start_datetime, :end_datetime]
)
# Should include all 3 bookings (original + before + after)
assert length(bookings) == 3
end
test "only returns accepted bookings, not pending/rejected/cancelled",
%{space: space, test_date: test_date, start_of_month: start_date, end_of_month: end_date} do
# Create a regular requested booking (not walk-in) on day 10
pending_date = %{test_date | day: 10}
{:ok, _pending} =
BookingSystem.create_booking(
space.id,
nil,
pending_date,
~T[09:00:00],
~T[17:00:00],
"Pending",
"pending@example.com",
nil,
nil
)
# Create and reject a booking on day 11
rejected_date = %{test_date | day: 11}
{:ok, rejected} =
BookingSystem.create_booking(
space.id,
nil,
rejected_date,
~T[09:00:00],
~T[17:00:00],
"Rejected",
"rejected@example.com",
nil,
nil
)
{:ok, _} = BookingSystem.reject_booking(rejected, "Test reason")
month_start_datetime = DateTime.new!(start_date, ~T[00:00:00], "Etc/UTC")
month_end_datetime = DateTime.new!(Date.add(end_date, 1), ~T[00:00:00], "Etc/UTC")
{:ok, bookings} =
BookingSystem.search_bookings(
space.id,
month_start_datetime,
month_end_datetime,
[:accepted],
[:start_datetime, :end_datetime]
)
# Should only have the original accepted booking from setup
assert length(bookings) == 1
end
test "handles bookings at exact month boundaries",
%{space: space, start_of_month: start_date, end_of_month: end_date} do
# Booking exactly at month start
month_start_datetime = DateTime.new!(start_date, ~T[00:00:00], "Etc/UTC")
month_start_end = DateTime.new!(start_date, ~T[08:00:00], "Etc/UTC")
{:ok, _start} =
BookingSystem.create_walk_in(
space.id,
month_start_datetime,
month_start_end,
"Start Boundary",
"start@example.com",
nil,
nil
)
# Booking exactly at month end
month_end_start = DateTime.new!(end_date, ~T[18:00:00], "Etc/UTC")
month_end_datetime = DateTime.new!(end_date, ~T[23:59:59], "Etc/UTC")
{:ok, _end} =
BookingSystem.create_walk_in(
space.id,
month_end_start,
month_end_datetime,
"End Boundary",
"end@example.com",
nil,
nil
)
month_start_datetime = DateTime.new!(start_date, ~T[00:00:00], "Etc/UTC")
month_end_datetime = DateTime.new!(Date.add(end_date, 1), ~T[00:00:00], "Etc/UTC")
{:ok, bookings} =
BookingSystem.search_bookings(
space.id,
month_start_datetime,
month_end_datetime,
[:accepted],
[:start_datetime, :end_datetime]
)
# Should include all bookings including boundaries
assert length(bookings) >= 3
end
test "filters by space_id correctly",
%{space: space, test_date: test_date, start_of_month: start_date, end_of_month: end_date} do
# Create another space
{:ok, other_space} = BookingSystem.create_space("Other", "other", "Other space", 5)
# Create booking for other space on day 16
other_date = %{test_date | day: 16}
start_datetime = DateTime.new!(other_date, ~T[09:00:00], "Etc/UTC")
end_datetime = DateTime.new!(other_date, ~T[17:00:00], "Etc/UTC")
{:ok, _other_booking} =
BookingSystem.create_walk_in(
other_space.id,
start_datetime,
end_datetime,
"Other Space",
"other@example.com",
nil,
nil
)
# Query for original space
month_start_datetime = DateTime.new!(start_date, ~T[00:00:00], "Etc/UTC")
month_end_datetime = DateTime.new!(Date.add(end_date, 1), ~T[00:00:00], "Etc/UTC")
{:ok, bookings} =
BookingSystem.search_bookings(
space.id,
month_start_datetime,
month_end_datetime,
[:accepted],
[:start_datetime, :end_datetime]
)
# Should only return bookings for the original space
assert length(bookings) == 1
end
end
end

View file

@ -311,7 +311,7 @@ defmodule SpazioSolazzo.BookingSystem.BookingTest do
end
end
describe "list_accepted_space_bookings_by_date/2" do
describe "search_bookings/5 for accepted bookings" do
test "returns only approved bookings for specific date", %{space: space, date: date} do
{:ok, approved1} =
request_booking(
@ -356,7 +356,17 @@ defmodule SpazioSolazzo.BookingSystem.BookingTest do
""
)
{:ok, bookings} = BookingSystem.list_accepted_space_bookings_by_date(space.id, date)
start_datetime = DateTime.new!(date, ~T[00:00:00], "Etc/UTC")
end_datetime = DateTime.new!(Date.add(date, 1), ~T[00:00:00], "Etc/UTC")
{:ok, bookings} =
BookingSystem.search_bookings(
space.id,
start_datetime,
end_datetime,
[:accepted],
nil
)
assert length(bookings) == 2
assert Enum.all?(bookings, &(&1.state == :accepted))
@ -379,7 +389,17 @@ defmodule SpazioSolazzo.BookingSystem.BookingTest do
{:ok, _} = BookingSystem.approve_booking(booking.id)
{:ok, _} = BookingSystem.cancel_booking(booking.id, "Test cancellation")
{:ok, bookings} = BookingSystem.list_accepted_space_bookings_by_date(space.id, date)
start_datetime = DateTime.new!(date, ~T[00:00:00], "Etc/UTC")
end_datetime = DateTime.new!(Date.add(date, 1), ~T[00:00:00], "Etc/UTC")
{:ok, bookings} =
BookingSystem.search_bookings(
space.id,
start_datetime,
end_datetime,
[:accepted],
nil
)
assert bookings == []
end
@ -417,7 +437,17 @@ defmodule SpazioSolazzo.BookingSystem.BookingTest do
{:ok, _} = BookingSystem.approve_booking(booking2.id)
{:ok, bookings} = BookingSystem.list_accepted_space_bookings_by_date(space.id, date)
start_datetime = DateTime.new!(date, ~T[00:00:00], "Etc/UTC")
end_datetime = DateTime.new!(Date.add(date, 1), ~T[00:00:00], "Etc/UTC")
{:ok, bookings} =
BookingSystem.search_bookings(
space.id,
start_datetime,
end_datetime,
[:accepted],
nil
)
assert length(bookings) == 1
assert hd(bookings).date == date
@ -447,13 +477,23 @@ defmodule SpazioSolazzo.BookingSystem.BookingTest do
{:ok, _} = BookingSystem.approve_booking(booking.id)
{:ok, bookings} = BookingSystem.list_accepted_space_bookings_by_date(space.id, date)
start_datetime = DateTime.new!(date, ~T[00:00:00], "Etc/UTC")
end_datetime = DateTime.new!(Date.add(date, 1), ~T[00:00:00], "Etc/UTC")
{:ok, bookings} =
BookingSystem.search_bookings(
space.id,
start_datetime,
end_datetime,
[:accepted],
nil
)
assert bookings == []
end
end
describe "list_booking_requests/3" do
describe "admin_search_bookings/3" do
test "returns pending and approved bookings for space", %{space: space, date: date} do
{:ok, pending} =
request_booking(
@ -498,7 +538,7 @@ defmodule SpazioSolazzo.BookingSystem.BookingTest do
{:ok, _} = BookingSystem.cancel_booking(cancelled.id, "Test cancellation")
{:ok, bookings} = BookingSystem.list_booking_requests(space.id, nil, nil)
{:ok, bookings} = BookingSystem.admin_search_bookings(space.id, nil, nil)
assert length(bookings) == 2
assert Enum.any?(bookings, &(&1.id == pending.id))
@ -534,7 +574,7 @@ defmodule SpazioSolazzo.BookingSystem.BookingTest do
)
{:ok, bookings} =
BookingSystem.list_booking_requests(space.id, "user2@example.com", nil)
BookingSystem.admin_search_bookings(space.id, "user2@example.com", nil)
assert length(bookings) == 1
assert hd(bookings).id == booking2.id
@ -569,14 +609,14 @@ defmodule SpazioSolazzo.BookingSystem.BookingTest do
""
)
{:ok, bookings} = BookingSystem.list_booking_requests(space.id, nil, date)
{:ok, bookings} = BookingSystem.admin_search_bookings(space.id, nil, date)
assert length(bookings) == 1
assert hd(bookings).id == booking1.id
end
end
describe "count_pending_requests/0" do
describe "count pending requests" do
test "returns only pending bookings", %{space: space, date: date} do
{:ok, _pending1} =
request_booking(
@ -621,13 +661,15 @@ defmodule SpazioSolazzo.BookingSystem.BookingTest do
{:ok, _} = BookingSystem.cancel_booking(cancelled.id, "Test cancellation")
{:ok, pending_requests} = BookingSystem.count_pending_requests()
{:ok, count} =
Ash.count(SpazioSolazzo.BookingSystem.Booking,
query: [filter: [state: :requested]]
)
assert length(pending_requests) == 1
assert Enum.all?(pending_requests, &(&1.state == :requested))
assert count == 1
end
test "returns empty list when no pending requests", %{space: space, date: date} do
test "returns zero when no pending requests", %{space: space, date: date} do
{:ok, booking} =
request_booking(
space.id,
@ -643,9 +685,12 @@ defmodule SpazioSolazzo.BookingSystem.BookingTest do
{:ok, _} = BookingSystem.approve_booking(booking.id)
{:ok, pending_requests} = BookingSystem.count_pending_requests()
{:ok, count} =
Ash.count(SpazioSolazzo.BookingSystem.Booking,
query: [filter: [state: :requested]]
)
assert pending_requests == []
assert count == 0
end
test "counts pending requests across multiple spaces", %{space: space, date: date} do
@ -683,9 +728,12 @@ defmodule SpazioSolazzo.BookingSystem.BookingTest do
""
)
{:ok, pending_requests} = BookingSystem.count_pending_requests()
{:ok, count} =
Ash.count(SpazioSolazzo.BookingSystem.Booking,
query: [filter: [state: :requested]]
)
assert length(pending_requests) == 2
assert count == 2
end
end

View file

@ -59,8 +59,17 @@ defmodule SpazioSolazzoWeb.Admin.WalkInLiveSimpleTest do
assert html =~ "Walk-in booking created successfully"
# Verify booking was created
start_datetime = DateTime.new!(tomorrow, ~T[00:00:00], "Etc/UTC")
end_datetime = DateTime.new!(Date.add(tomorrow, 1), ~T[00:00:00], "Etc/UTC")
assert {:ok, [_booking]} =
BookingSystem.list_accepted_space_bookings_by_date(space.id, tomorrow)
BookingSystem.search_bookings(
space.id,
start_datetime,
end_datetime,
[:accepted],
nil
)
end
end
end

View file

@ -73,7 +73,18 @@ defmodule SpazioSolazzoWeb.Admin.WalkInLiveTest do
assert html =~ "Walk-in booking created successfully"
# Verify booking was created
{:ok, bookings} = BookingSystem.list_accepted_space_bookings_by_date(space.id, tomorrow)
start_datetime = DateTime.new!(tomorrow, ~T[00:00:00], "Etc/UTC")
end_datetime = DateTime.new!(Date.add(tomorrow, 1), ~T[00:00:00], "Etc/UTC")
{:ok, bookings} =
BookingSystem.search_bookings(
space.id,
start_datetime,
end_datetime,
[:accepted],
nil
)
assert length(bookings) == 1
booking = hd(bookings)
assert booking.customer_name == "John Doe"
@ -174,19 +185,36 @@ defmodule SpazioSolazzoWeb.Admin.WalkInLiveTest do
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)
start_datetime = DateTime.new!(start_date, ~T[00:00:00], "Etc/UTC")
end_datetime_search = DateTime.new!(Date.add(start_date, 1), ~T[00:00:00], "Etc/UTC")
{:ok, bookings} =
BookingSystem.search_bookings(
space.id,
start_datetime,
end_datetime_search,
[:accepted],
nil
)
assert length(bookings) == 1
booking = hd(bookings)
assert booking.customer_name == "Jane Smith"
# Verify booking appears on all days in the range
day2_start = DateTime.new!(Date.add(start_date, 1), ~T[00:00:00], "Etc/UTC")
day2_end = DateTime.new!(Date.add(start_date, 2), ~T[00:00:00], "Etc/UTC")
{:ok, day2_bookings} =
BookingSystem.list_accepted_space_bookings_by_date(space.id, Date.add(start_date, 1))
BookingSystem.search_bookings(space.id, day2_start, day2_end, [:accepted], nil)
assert length(day2_bookings) == 1
day3_start = DateTime.new!(end_date, ~T[00:00:00], "Etc/UTC")
day3_end = DateTime.new!(Date.add(end_date, 1), ~T[00:00:00], "Etc/UTC")
{:ok, day3_bookings} =
BookingSystem.list_accepted_space_bookings_by_date(space.id, end_date)
BookingSystem.search_bookings(space.id, day3_start, day3_end, [:accepted], nil)
assert length(day3_bookings) == 1
end
@ -215,7 +243,18 @@ defmodule SpazioSolazzoWeb.Admin.WalkInLiveTest do
assert html =~ "Walk-in booking created successfully"
{:ok, bookings} = BookingSystem.list_accepted_space_bookings_by_date(space.id, tomorrow)
start_datetime = DateTime.new!(tomorrow, ~T[00:00:00], "Etc/UTC")
end_datetime = DateTime.new!(Date.add(tomorrow, 1), ~T[00:00:00], "Etc/UTC")
{:ok, bookings} =
BookingSystem.search_bookings(
space.id,
start_datetime,
end_datetime,
[:accepted],
nil
)
booking = hd(bookings)
assert booking.customer_phone == "+39 1234567890"
assert booking.customer_comment == "Special request"

View file

@ -394,7 +394,7 @@ defmodule SpazioSolazzoWeb.BookingLive.SpaceBookingTest do
# Process the handle_info message that creates the booking
render(view)
{:ok, bookings} = BookingSystem.list_booking_requests(space.id, nil, today)
{:ok, bookings} = BookingSystem.admin_search_bookings(space.id, nil, today)
assert length(bookings) == 1
booking = hd(bookings)
@ -578,7 +578,7 @@ defmodule SpazioSolazzoWeb.BookingLive.SpaceBookingTest do
# Process the handle_info message that creates the booking
render(view)
{:ok, bookings} = BookingSystem.list_booking_requests(space.id, nil, today)
{:ok, bookings} = BookingSystem.admin_search_bookings(space.id, nil, today)
assert length(bookings) == 1
booking = hd(bookings)