mirror of
https://codeberg.org/JasterV/spazio-solazzo.git
synced 2026-04-26 18:20:03 +00:00
feat: new booking system
This commit is contained in:
parent
bbc2f08215
commit
0f019615b2
63 changed files with 3820 additions and 2045 deletions
|
|
@ -2,7 +2,7 @@ defmodule SpazioSolazzo.Accounts.User.Changes.HandleBookingsOnAccountDeletion do
|
|||
@moduledoc """
|
||||
Handles booking cleanup when a user account is terminated.
|
||||
|
||||
- Cancels all future reserved bookings
|
||||
- Cancels all future requested/accepted bookings with a reason
|
||||
- Either deletes all bookings or lets the database nullify them based on delete_history argument
|
||||
"""
|
||||
use Ash.Resource.Change
|
||||
|
|
@ -16,16 +16,21 @@ defmodule SpazioSolazzo.Accounts.User.Changes.HandleBookingsOnAccountDeletion do
|
|||
user = changeset.data
|
||||
delete_history = Ash.Changeset.get_argument(changeset, :delete_history)
|
||||
|
||||
Booking
|
||||
|> Ash.Query.filter(
|
||||
user_id == ^user.id and state == :reserved and date >= ^Date.utc_today()
|
||||
)
|
||||
|> BookingSystem.cancel_booking!()
|
||||
future_bookings =
|
||||
Booking
|
||||
|> Ash.Query.filter(
|
||||
user_id == ^user.id and state in [:requested, :accepted] and date >= ^Date.utc_today()
|
||||
)
|
||||
|> Ash.read!()
|
||||
|
||||
Enum.each(future_bookings, fn booking ->
|
||||
BookingSystem.cancel_booking!(booking, "Account deleted by user")
|
||||
end)
|
||||
|
||||
if delete_history do
|
||||
Booking
|
||||
|> Ash.Query.filter(user_id == ^user.id)
|
||||
|> BookingSystem.delete_booking!(authorize?: false)
|
||||
|> Ash.bulk_destroy!(:destroy, %{}, authorize?: false)
|
||||
end
|
||||
|
||||
changeset
|
||||
|
|
|
|||
|
|
@ -1,22 +1,21 @@
|
|||
defmodule SpazioSolazzo.BookingSystem do
|
||||
@moduledoc """
|
||||
Manages bookings, spaces, assets and time slots for the booking system.
|
||||
Manages bookings, spaces, and time slots for the booking system.
|
||||
"""
|
||||
|
||||
use Ash.Domain,
|
||||
otp_app: :spazio_solazzo
|
||||
|
||||
require Ash.Query
|
||||
alias SpazioSolazzo.BookingSystem.Space
|
||||
|
||||
resources do
|
||||
resource SpazioSolazzo.BookingSystem.Space do
|
||||
define :get_space_by_slug, action: :read, get_by: [:slug]
|
||||
define :create_space, action: :create, args: [:name, :slug, :description]
|
||||
end
|
||||
|
||||
resource SpazioSolazzo.BookingSystem.Asset do
|
||||
define :get_asset_by_id, action: :read, get_by: [:id]
|
||||
define :get_asset_by_space_id, action: :read, get_by: [:space_id]
|
||||
define :get_space_assets, action: :get_space_assets, args: [:space_id]
|
||||
define :create_asset, action: :create, args: [:name, :space_id]
|
||||
define :create_space,
|
||||
action: :create,
|
||||
args: [:name, :slug, :description, :public_capacity, :real_capacity]
|
||||
end
|
||||
|
||||
resource SpazioSolazzo.BookingSystem.TimeSlotTemplate do
|
||||
|
|
@ -30,26 +29,103 @@ defmodule SpazioSolazzo.BookingSystem do
|
|||
end
|
||||
|
||||
resource SpazioSolazzo.BookingSystem.Booking do
|
||||
define :list_active_asset_bookings_by_date,
|
||||
action: :list_active_asset_bookings_by_date,
|
||||
args: [:asset_id, :date]
|
||||
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,
|
||||
args: [:space_id, :email, :date]
|
||||
|
||||
define :create_booking,
|
||||
action: :create,
|
||||
args: [
|
||||
:time_slot_template_id,
|
||||
:asset_id,
|
||||
:space_id,
|
||||
:user_id,
|
||||
:date,
|
||||
:start_time,
|
||||
:end_time,
|
||||
:customer_name,
|
||||
:customer_email,
|
||||
:customer_phone,
|
||||
:customer_comment
|
||||
]
|
||||
|
||||
define :confirm_booking, action: :confirm_booking, args: []
|
||||
define :cancel_booking, action: :cancel, args: []
|
||||
define :approve_booking, action: :approve, args: []
|
||||
define :reject_booking, action: :reject, args: [:reason]
|
||||
define :cancel_booking, action: :cancel, args: [:reason]
|
||||
define :delete_booking, action: :destroy, args: []
|
||||
end
|
||||
end
|
||||
|
||||
def request_booking(space_id, user_id, date, start_time, end_time, customer_details) do
|
||||
create_booking(
|
||||
space_id,
|
||||
user_id,
|
||||
date,
|
||||
start_time,
|
||||
end_time,
|
||||
customer_details.name,
|
||||
customer_details.email,
|
||||
customer_details[:phone],
|
||||
customer_details[:comment]
|
||||
)
|
||||
end
|
||||
|
||||
def create_walk_in(space_id, customer_details, start_datetime, end_datetime) do
|
||||
date = DateTime.to_date(start_datetime)
|
||||
start_time = DateTime.to_time(start_datetime)
|
||||
end_time = DateTime.to_time(end_datetime)
|
||||
|
||||
case create_booking(
|
||||
space_id,
|
||||
nil,
|
||||
date,
|
||||
start_time,
|
||||
end_time,
|
||||
customer_details.name,
|
||||
customer_details.email,
|
||||
customer_details[:phone],
|
||||
customer_details[:comment]
|
||||
) do
|
||||
{:ok, booking} ->
|
||||
approve_booking!(booking)
|
||||
{:ok, booking}
|
||||
|
||||
error ->
|
||||
error
|
||||
end
|
||||
end
|
||||
|
||||
def check_availability(space_id, date, start_time, end_time) do
|
||||
with {:ok, space} <- Ash.get(Space, space_id),
|
||||
{:ok, bookings} <- list_accepted_space_bookings_by_date(space_id, date) do
|
||||
overlapping_bookings =
|
||||
Enum.filter(bookings, fn booking ->
|
||||
times_overlap?(
|
||||
booking.start_time,
|
||||
booking.end_time,
|
||||
start_time,
|
||||
end_time
|
||||
)
|
||||
end)
|
||||
|
||||
current_count = length(overlapping_bookings)
|
||||
|
||||
cond do
|
||||
current_count >= space.real_capacity ->
|
||||
{:ok, :over_real_capacity}
|
||||
|
||||
current_count >= space.public_capacity ->
|
||||
{:ok, :over_public_capacity}
|
||||
|
||||
true ->
|
||||
{:ok, :available}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defp times_overlap?(start1, end1, start2, end2) do
|
||||
Time.compare(start1, end2) == :lt and Time.compare(start2, end1) == :lt
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -1,43 +0,0 @@
|
|||
defmodule SpazioSolazzo.BookingSystem.Asset do
|
||||
@moduledoc """
|
||||
Represents bookable assets within a space, such as rooms or equipment.
|
||||
"""
|
||||
|
||||
use Ash.Resource,
|
||||
otp_app: :spazio_solazzo,
|
||||
domain: SpazioSolazzo.BookingSystem,
|
||||
data_layer: AshPostgres.DataLayer
|
||||
|
||||
postgres do
|
||||
table "assets"
|
||||
repo SpazioSolazzo.Repo
|
||||
end
|
||||
|
||||
actions do
|
||||
defaults [:read, create: :*]
|
||||
|
||||
read :get_space_assets do
|
||||
argument :space_id, :string do
|
||||
allow_nil? false
|
||||
end
|
||||
|
||||
filter expr(space_id == ^arg(:space_id))
|
||||
end
|
||||
end
|
||||
|
||||
attributes do
|
||||
uuid_primary_key :id
|
||||
attribute :name, :string, allow_nil?: false, public?: true
|
||||
end
|
||||
|
||||
relationships do
|
||||
belongs_to :space, SpazioSolazzo.BookingSystem.Space do
|
||||
allow_nil? false
|
||||
public? true
|
||||
end
|
||||
end
|
||||
|
||||
identities do
|
||||
identity :unique_name_per_space, [:name, :space_id]
|
||||
end
|
||||
end
|
||||
|
|
@ -11,7 +11,13 @@ defmodule SpazioSolazzo.BookingSystem.Booking do
|
|||
authorizers: [Ash.Policy.Authorizer],
|
||||
extensions: [AshStateMachine]
|
||||
|
||||
alias SpazioSolazzo.BookingSystem.Booking.EmailWorker
|
||||
require Ash.Query
|
||||
|
||||
alias SpazioSolazzo.BookingSystem.Booking.{
|
||||
NewRequestWorker,
|
||||
DecisionWorker,
|
||||
CancellationWorker
|
||||
}
|
||||
|
||||
postgres do
|
||||
table "bookings"
|
||||
|
|
@ -23,109 +29,250 @@ defmodule SpazioSolazzo.BookingSystem.Booking do
|
|||
end
|
||||
|
||||
state_machine do
|
||||
initial_states([:reserved])
|
||||
default_initial_state(:reserved)
|
||||
initial_states([:requested])
|
||||
default_initial_state(:requested)
|
||||
|
||||
transitions do
|
||||
transition(:confirm_booking, from: :reserved, to: :completed)
|
||||
transition(:cancel, from: :reserved, to: :cancelled)
|
||||
transition(:approve, from: :requested, to: :accepted)
|
||||
transition(:reject, from: :requested, to: :rejected)
|
||||
transition(:cancel, from: [:requested, :accepted], to: :cancelled)
|
||||
end
|
||||
end
|
||||
|
||||
actions do
|
||||
defaults [:read]
|
||||
|
||||
read :list_active_asset_bookings_by_date do
|
||||
argument :asset_id, :uuid, allow_nil?: false
|
||||
read :list_accepted_space_bookings_by_date do
|
||||
argument :space_id, :uuid, allow_nil?: false
|
||||
argument :date, :date, allow_nil?: false
|
||||
|
||||
filter expr(
|
||||
asset_id == ^arg(:asset_id) and date == ^arg(:date) and
|
||||
state in [:reserved, :completed]
|
||||
space_id == ^arg(:space_id) and date == ^arg(:date) and
|
||||
state == :accepted
|
||||
)
|
||||
end
|
||||
|
||||
read :list_booking_requests do
|
||||
argument :space_id, :uuid, allow_nil?: true
|
||||
argument :email, :string, allow_nil?: true
|
||||
argument :date, :date, allow_nil?: true
|
||||
|
||||
filter expr(state == :requested or state == :accepted)
|
||||
|
||||
prepare fn query, _ctx ->
|
||||
query
|
||||
|> then(fn q ->
|
||||
case Ash.Query.get_argument(q, :space_id) do
|
||||
nil -> q
|
||||
space_id -> Ash.Query.filter(q, space_id == ^space_id)
|
||||
end
|
||||
end)
|
||||
|> then(fn q ->
|
||||
case Ash.Query.get_argument(q, :email) do
|
||||
nil -> q
|
||||
email -> Ash.Query.filter(q, customer_email == ^email)
|
||||
end
|
||||
end)
|
||||
|> then(fn q ->
|
||||
case Ash.Query.get_argument(q, :date) do
|
||||
nil -> q
|
||||
date -> Ash.Query.filter(q, date == ^date)
|
||||
end
|
||||
end)
|
||||
end
|
||||
end
|
||||
|
||||
create :create do
|
||||
argument :time_slot_template_id, :uuid, allow_nil?: false
|
||||
argument :asset_id, :uuid, allow_nil?: false
|
||||
argument :user_id, :uuid, allow_nil?: false
|
||||
argument :space_id, :uuid, allow_nil?: false
|
||||
argument :user_id, :uuid, allow_nil?: true
|
||||
argument :date, :date, allow_nil?: false
|
||||
argument :start_time, :time, allow_nil?: false
|
||||
argument :end_time, :time, allow_nil?: false
|
||||
argument :customer_name, :string, allow_nil?: false
|
||||
argument :customer_email, :string, allow_nil?: false
|
||||
argument :customer_phone, :string, allow_nil?: true
|
||||
argument :customer_comment, :string, allow_nil?: true
|
||||
|
||||
change manage_relationship(:time_slot_template_id, :time_slot_template,
|
||||
type: :append_and_remove
|
||||
)
|
||||
|
||||
change manage_relationship(:asset_id, :asset, type: :append_and_remove)
|
||||
change manage_relationship(:space_id, :space, type: :append_and_remove)
|
||||
|
||||
change manage_relationship(:user_id, :user, type: :append_and_remove, authorize?: false)
|
||||
|
||||
change fn changeset, _ctx ->
|
||||
template_id = Ash.Changeset.get_argument(changeset, :time_slot_template_id)
|
||||
validate fn changeset, _ctx ->
|
||||
date = Ash.Changeset.get_argument(changeset, :date)
|
||||
today = Date.utc_today()
|
||||
|
||||
case Ash.get(SpazioSolazzo.BookingSystem.TimeSlotTemplate, template_id) do
|
||||
{:ok, template} ->
|
||||
changeset
|
||||
|> Ash.Changeset.force_change_attribute(:start_time, template.start_time)
|
||||
|> Ash.Changeset.force_change_attribute(:end_time, template.end_time)
|
||||
|> Ash.Changeset.force_change_attribute(
|
||||
:date,
|
||||
Ash.Changeset.get_argument(changeset, :date)
|
||||
)
|
||||
|> Ash.Changeset.force_change_attribute(
|
||||
:customer_name,
|
||||
Ash.Changeset.get_argument(changeset, :customer_name)
|
||||
)
|
||||
|> Ash.Changeset.force_change_attribute(
|
||||
:customer_email,
|
||||
Ash.Changeset.get_argument(changeset, :customer_email)
|
||||
)
|
||||
|> Ash.Changeset.force_change_attribute(
|
||||
:customer_phone,
|
||||
Ash.Changeset.get_argument(changeset, :customer_phone)
|
||||
)
|
||||
|> Ash.Changeset.force_change_attribute(
|
||||
:customer_comment,
|
||||
Ash.Changeset.get_argument(changeset, :customer_comment)
|
||||
)
|
||||
|
||||
{:error, _} ->
|
||||
Ash.Changeset.add_error(changeset,
|
||||
field: :time_slot_template_id,
|
||||
message: "Template not found"
|
||||
)
|
||||
if date && Date.compare(date, today) == :lt do
|
||||
{:error, field: :date, message: "cannot be in the past"}
|
||||
else
|
||||
:ok
|
||||
end
|
||||
end
|
||||
|
||||
validate fn changeset, _ctx ->
|
||||
start_time = Ash.Changeset.get_argument(changeset, :start_time)
|
||||
end_time = Ash.Changeset.get_argument(changeset, :end_time)
|
||||
|
||||
if start_time && end_time && Time.compare(end_time, start_time) != :gt do
|
||||
{:error, field: :end_time, message: "must be after start time"}
|
||||
else
|
||||
:ok
|
||||
end
|
||||
end
|
||||
|
||||
validate fn changeset, _ctx ->
|
||||
email = Ash.Changeset.get_argument(changeset, :customer_email)
|
||||
|
||||
if email && !String.match?(email, ~r/^[^\s@]+@[^\s@]+\.[^\s@]+$/) do
|
||||
{:error, field: :customer_email, message: "must be a valid email"}
|
||||
else
|
||||
:ok
|
||||
end
|
||||
end
|
||||
|
||||
change fn changeset, _ctx ->
|
||||
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(
|
||||
:customer_name,
|
||||
Ash.Changeset.get_argument(changeset, :customer_name)
|
||||
)
|
||||
|> Ash.Changeset.force_change_attribute(
|
||||
:customer_email,
|
||||
Ash.Changeset.get_argument(changeset, :customer_email)
|
||||
)
|
||||
|> Ash.Changeset.force_change_attribute(
|
||||
:customer_phone,
|
||||
Ash.Changeset.get_argument(changeset, :customer_phone)
|
||||
)
|
||||
|> Ash.Changeset.force_change_attribute(
|
||||
:customer_comment,
|
||||
Ash.Changeset.get_argument(changeset, :customer_comment)
|
||||
)
|
||||
end
|
||||
|
||||
change after_action(fn _changeset, booking, _ctx ->
|
||||
booking = Ash.load!(booking, [:space])
|
||||
|
||||
%{
|
||||
booking_id: booking.id,
|
||||
customer_name: booking.customer_name,
|
||||
customer_email: booking.customer_email,
|
||||
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
|
||||
}
|
||||
|> EmailWorker.new()
|
||||
|> NewRequestWorker.new()
|
||||
|> Oban.insert!()
|
||||
|
||||
{:ok, booking}
|
||||
end)
|
||||
end
|
||||
|
||||
update :confirm_booking do
|
||||
update :approve do
|
||||
accept []
|
||||
change transition_state(:completed)
|
||||
require_atomic? false
|
||||
change transition_state(:accepted)
|
||||
|
||||
change after_action(fn _changeset, booking, _ctx ->
|
||||
booking = Ash.load!(booking, [:space])
|
||||
|
||||
%{
|
||||
booking_id: booking.id,
|
||||
customer_name: booking.customer_name,
|
||||
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,
|
||||
decision: "accepted",
|
||||
rejection_reason: nil
|
||||
}
|
||||
|> DecisionWorker.new()
|
||||
|> Oban.insert!()
|
||||
|
||||
{:ok, booking}
|
||||
end)
|
||||
end
|
||||
|
||||
update :reject do
|
||||
accept [:rejection_reason]
|
||||
argument :reason, :string, allow_nil?: false
|
||||
require_atomic? false
|
||||
|
||||
change fn changeset, _ctx ->
|
||||
reason = Ash.Changeset.get_argument(changeset, :reason)
|
||||
Ash.Changeset.force_change_attribute(changeset, :rejection_reason, reason)
|
||||
end
|
||||
|
||||
change transition_state(:rejected)
|
||||
|
||||
change after_action(fn _changeset, booking, _ctx ->
|
||||
booking = Ash.load!(booking, [:space])
|
||||
|
||||
%{
|
||||
booking_id: booking.id,
|
||||
customer_name: booking.customer_name,
|
||||
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,
|
||||
decision: "rejected",
|
||||
rejection_reason: booking.rejection_reason
|
||||
}
|
||||
|> DecisionWorker.new()
|
||||
|> Oban.insert!()
|
||||
|
||||
{:ok, booking}
|
||||
end)
|
||||
end
|
||||
|
||||
update :cancel do
|
||||
accept []
|
||||
accept [:cancellation_reason]
|
||||
argument :reason, :string, allow_nil?: false
|
||||
require_atomic? false
|
||||
|
||||
change fn changeset, _ctx ->
|
||||
reason = Ash.Changeset.get_argument(changeset, :reason)
|
||||
Ash.Changeset.force_change_attribute(changeset, :cancellation_reason, reason)
|
||||
end
|
||||
|
||||
change transition_state(:cancelled)
|
||||
|
||||
change after_action(fn _changeset, booking, _ctx ->
|
||||
booking = Ash.load!(booking, [:space])
|
||||
|
||||
%{
|
||||
customer_name: booking.customer_name,
|
||||
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,
|
||||
cancellation_reason: booking.cancellation_reason
|
||||
}
|
||||
|> CancellationWorker.new()
|
||||
|> Oban.insert!()
|
||||
|
||||
{:ok, booking}
|
||||
end)
|
||||
end
|
||||
|
||||
destroy :destroy do
|
||||
|
|
@ -135,7 +282,7 @@ defmodule SpazioSolazzo.BookingSystem.Booking do
|
|||
end
|
||||
|
||||
policies do
|
||||
policy action([:cancel, :confirm_booking]) do
|
||||
policy action([:cancel, :approve, :reject]) do
|
||||
authorize_if always()
|
||||
end
|
||||
|
||||
|
|
@ -157,6 +304,8 @@ defmodule SpazioSolazzo.BookingSystem.Booking do
|
|||
prefix "booking"
|
||||
|
||||
publish :create, ["created"]
|
||||
publish :approve, ["approved"]
|
||||
publish :reject, ["rejected"]
|
||||
publish :cancel, ["cancelled"]
|
||||
end
|
||||
|
||||
|
|
@ -169,12 +318,14 @@ defmodule SpazioSolazzo.BookingSystem.Booking do
|
|||
attribute :end_time, :time, allow_nil?: false
|
||||
attribute :customer_phone, :string, allow_nil?: true
|
||||
attribute :customer_comment, :string, allow_nil?: true
|
||||
attribute :cancellation_reason, :string, allow_nil?: true
|
||||
attribute :rejection_reason, :string, allow_nil?: true
|
||||
|
||||
attribute :state, :atom do
|
||||
allow_nil? false
|
||||
default :reserved
|
||||
default :requested
|
||||
public? true
|
||||
constraints one_of: [:reserved, :completed, :cancelled]
|
||||
constraints one_of: [:requested, :accepted, :rejected, :cancelled]
|
||||
end
|
||||
|
||||
create_timestamp :inserted_at
|
||||
|
|
@ -182,8 +333,10 @@ defmodule SpazioSolazzo.BookingSystem.Booking do
|
|||
end
|
||||
|
||||
relationships do
|
||||
belongs_to :asset, SpazioSolazzo.BookingSystem.Asset
|
||||
belongs_to :time_slot_template, SpazioSolazzo.BookingSystem.TimeSlotTemplate
|
||||
belongs_to :space, SpazioSolazzo.BookingSystem.Space do
|
||||
allow_nil? false
|
||||
public? true
|
||||
end
|
||||
|
||||
belongs_to :user, SpazioSolazzo.Accounts.User do
|
||||
allow_nil? true
|
||||
|
|
|
|||
|
|
@ -0,0 +1,43 @@
|
|||
defmodule SpazioSolazzo.BookingSystem.Booking.CancellationWorker do
|
||||
@moduledoc """
|
||||
Sends cancellation notification emails to administrators when a customer cancels a booking.
|
||||
"""
|
||||
|
||||
use Oban.Worker, queue: :booking_email, max_attempts: 3
|
||||
|
||||
alias SpazioSolazzo.BookingSystem.Booking.Email
|
||||
|
||||
@impl Oban.Worker
|
||||
def perform(%Oban.Job{
|
||||
args: %{
|
||||
"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,
|
||||
"cancellation_reason" => cancellation_reason
|
||||
}
|
||||
}) do
|
||||
%{
|
||||
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,
|
||||
cancellation_reason: cancellation_reason,
|
||||
admin_email: admin_email()
|
||||
}
|
||||
|> Email.cancelled_admin()
|
||||
|> SpazioSolazzo.Mailer.deliver!()
|
||||
|
||||
:ok
|
||||
end
|
||||
|
||||
defp admin_email do
|
||||
Application.get_env(:spazio_solazzo, :admin_email)
|
||||
end
|
||||
end
|
||||
56
lib/spazio_solazzo/booking_system/booking/decision_worker.ex
Normal file
56
lib/spazio_solazzo/booking_system/booking/decision_worker.ex
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
defmodule SpazioSolazzo.BookingSystem.Booking.DecisionWorker do
|
||||
@moduledoc """
|
||||
Sends emails when an admin approves or rejects a booking request.
|
||||
"""
|
||||
|
||||
use Oban.Worker, queue: :booking_email, max_attempts: 3
|
||||
|
||||
alias SpazioSolazzo.BookingSystem.Booking.Email
|
||||
|
||||
@impl Oban.Worker
|
||||
def perform(%Oban.Job{
|
||||
args: %{
|
||||
"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,
|
||||
"decision" => decision,
|
||||
"rejection_reason" => rejection_reason
|
||||
}
|
||||
}) do
|
||||
case decision do
|
||||
"accepted" ->
|
||||
%{
|
||||
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
|
||||
}
|
||||
|> Email.accepted_user()
|
||||
|> SpazioSolazzo.Mailer.deliver!()
|
||||
|
||||
"rejected" ->
|
||||
%{
|
||||
customer_name: customer_name,
|
||||
customer_email: customer_email,
|
||||
space_name: space_name,
|
||||
date: date,
|
||||
start_time: start_time,
|
||||
end_time: end_time,
|
||||
rejection_reason: rejection_reason
|
||||
}
|
||||
|> Email.rejected_user()
|
||||
|> SpazioSolazzo.Mailer.deliver!()
|
||||
end
|
||||
|
||||
:ok
|
||||
end
|
||||
end
|
||||
|
|
@ -11,6 +11,102 @@ defmodule SpazioSolazzo.BookingSystem.Booking.Email do
|
|||
use SpazioSolazzoWeb, :verified_routes
|
||||
alias SpazioSolazzo.BookingSystem.Booking.Token
|
||||
|
||||
def request_received_user(%{
|
||||
booking_id: booking_id,
|
||||
customer_name: customer_name,
|
||||
customer_email: customer_email,
|
||||
customer_phone: customer_phone,
|
||||
customer_comment: customer_comment,
|
||||
space_name: space_name,
|
||||
date: date,
|
||||
start_time: start_time,
|
||||
end_time: end_time
|
||||
}) do
|
||||
cancel_token = Token.generate_customer_cancel_token(booking_id)
|
||||
cancel_url = url(~p"/bookings/cancel?token=#{cancel_token}")
|
||||
|
||||
assigns = %{
|
||||
customer_name: customer_name,
|
||||
customer_email: customer_email,
|
||||
customer_phone: customer_phone,
|
||||
customer_comment: customer_comment,
|
||||
space_name: space_name,
|
||||
date: date,
|
||||
start_time: start_time,
|
||||
end_time: end_time,
|
||||
cancel_url: cancel_url,
|
||||
front_office_phone_number: front_office_phone_number(),
|
||||
subject: "Request Received: #{date}"
|
||||
}
|
||||
|
||||
new()
|
||||
|> to({customer_name, customer_email})
|
||||
|> from({"Spazio Solazzo", spazio_solazzo_email()})
|
||||
|> subject(assigns.subject)
|
||||
|> render_body("request_received_user.html", assigns)
|
||||
end
|
||||
|
||||
def accepted_user(%{
|
||||
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
|
||||
}) do
|
||||
cancel_token = Token.generate_customer_cancel_token(booking_id)
|
||||
cancel_url = url(~p"/bookings/cancel?token=#{cancel_token}")
|
||||
|
||||
assigns = %{
|
||||
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,
|
||||
cancel_url: cancel_url,
|
||||
front_office_phone_number: front_office_phone_number(),
|
||||
subject: "Booking Approved: #{date}"
|
||||
}
|
||||
|
||||
new()
|
||||
|> to({customer_name, customer_email})
|
||||
|> from({"Spazio Solazzo", spazio_solazzo_email()})
|
||||
|> subject(assigns.subject)
|
||||
|> render_body("accepted_user.html", assigns)
|
||||
end
|
||||
|
||||
def rejected_user(%{
|
||||
customer_name: customer_name,
|
||||
customer_email: customer_email,
|
||||
space_name: space_name,
|
||||
date: date,
|
||||
start_time: start_time,
|
||||
end_time: end_time,
|
||||
rejection_reason: rejection_reason
|
||||
}) do
|
||||
assigns = %{
|
||||
customer_name: customer_name,
|
||||
customer_email: customer_email,
|
||||
space_name: space_name,
|
||||
date: date,
|
||||
start_time: start_time,
|
||||
end_time: end_time,
|
||||
rejection_reason: rejection_reason,
|
||||
front_office_phone_number: front_office_phone_number(),
|
||||
subject: "Booking Request Update: #{date}"
|
||||
}
|
||||
|
||||
new()
|
||||
|> to({customer_name, customer_email})
|
||||
|> from({"Spazio Solazzo", spazio_solazzo_email()})
|
||||
|> subject(assigns.subject)
|
||||
|> render_body("rejected_user.html", assigns)
|
||||
end
|
||||
|
||||
def customer_confirmation(%{
|
||||
booking_id: booking_id,
|
||||
customer_name: customer_name,
|
||||
|
|
@ -44,6 +140,69 @@ defmodule SpazioSolazzo.BookingSystem.Booking.Email do
|
|||
|> render_body("customer_confirmation.html", assigns)
|
||||
end
|
||||
|
||||
def new_request_admin(%{
|
||||
customer_name: customer_name,
|
||||
customer_email: customer_email,
|
||||
customer_phone: customer_phone,
|
||||
customer_comment: customer_comment,
|
||||
space_name: space_name,
|
||||
date: date,
|
||||
start_time: start_time,
|
||||
end_time: end_time,
|
||||
admin_email: admin_email
|
||||
}) do
|
||||
dashboard_url = url(~p"/admin/dashboard")
|
||||
|
||||
assigns = %{
|
||||
customer_name: customer_name,
|
||||
customer_email: customer_email,
|
||||
customer_phone: customer_phone,
|
||||
customer_comment: customer_comment,
|
||||
space_name: space_name,
|
||||
date: date,
|
||||
start_time: start_time,
|
||||
end_time: end_time,
|
||||
dashboard_url: dashboard_url,
|
||||
subject: "New Booking Request: #{customer_name}"
|
||||
}
|
||||
|
||||
new()
|
||||
|> to(admin_email)
|
||||
|> from({"Spazio Solazzo", spazio_solazzo_email()})
|
||||
|> subject(assigns.subject)
|
||||
|> render_body("new_request_admin.html", assigns)
|
||||
end
|
||||
|
||||
def cancelled_admin(%{
|
||||
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,
|
||||
cancellation_reason: cancellation_reason,
|
||||
admin_email: admin_email
|
||||
}) do
|
||||
assigns = %{
|
||||
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,
|
||||
cancellation_reason: cancellation_reason,
|
||||
subject: "Booking Cancelled: #{customer_name}"
|
||||
}
|
||||
|
||||
new()
|
||||
|> to(admin_email)
|
||||
|> from({"Spazio Solazzo", spazio_solazzo_email()})
|
||||
|> subject(assigns.subject)
|
||||
|> render_body("cancelled_admin.html", assigns)
|
||||
end
|
||||
|
||||
# --- Admin Email ---
|
||||
def admin_notification(%{
|
||||
booking_id: booking_id,
|
||||
|
|
|
|||
|
|
@ -1,9 +1,10 @@
|
|||
defmodule SpazioSolazzo.BookingSystem.Booking.EmailWorker do
|
||||
defmodule SpazioSolazzo.BookingSystem.Booking.NewRequestWorker do
|
||||
@moduledoc """
|
||||
Sends booking confirmation emails to customers and notification emails to administrators.
|
||||
Sends booking request confirmation emails to customers and notification emails to administrators.
|
||||
Triggered when a new booking request is created.
|
||||
"""
|
||||
|
||||
use Oban.Worker, queue: :booking_email, max_attempts: 1
|
||||
use Oban.Worker, queue: :booking_email, max_attempts: 3
|
||||
|
||||
alias SpazioSolazzo.BookingSystem.Booking.Email
|
||||
|
||||
|
|
@ -15,6 +16,7 @@ defmodule SpazioSolazzo.BookingSystem.Booking.EmailWorker do
|
|||
"customer_email" => customer_email,
|
||||
"customer_phone" => customer_phone,
|
||||
"customer_comment" => customer_comment,
|
||||
"space_name" => space_name,
|
||||
"date" => date,
|
||||
"start_time" => start_time,
|
||||
"end_time" => end_time
|
||||
|
|
@ -26,6 +28,7 @@ defmodule SpazioSolazzo.BookingSystem.Booking.EmailWorker do
|
|||
customer_email: customer_email,
|
||||
customer_phone: customer_phone,
|
||||
customer_comment: customer_comment,
|
||||
space_name: space_name,
|
||||
date: date,
|
||||
start_time: start_time,
|
||||
end_time: end_time,
|
||||
|
|
@ -33,11 +36,11 @@ defmodule SpazioSolazzo.BookingSystem.Booking.EmailWorker do
|
|||
}
|
||||
|
||||
email_data
|
||||
|> Email.customer_confirmation()
|
||||
|> Email.request_received_user()
|
||||
|> SpazioSolazzo.Mailer.deliver!()
|
||||
|
||||
email_data
|
||||
|> Email.admin_notification()
|
||||
|> Email.new_request_admin()
|
||||
|> SpazioSolazzo.Mailer.deliver!()
|
||||
|
||||
:ok
|
||||
|
|
@ -14,7 +14,31 @@ defmodule SpazioSolazzo.BookingSystem.Space do
|
|||
end
|
||||
|
||||
actions do
|
||||
defaults [:read, create: :*]
|
||||
defaults [:read]
|
||||
|
||||
create :create do
|
||||
accept [:name, :description, :slug, :public_capacity, :real_capacity]
|
||||
|
||||
validate fn changeset, _ctx ->
|
||||
real_capacity = Ash.Changeset.get_attribute(changeset, :real_capacity)
|
||||
public_capacity = Ash.Changeset.get_attribute(changeset, :public_capacity)
|
||||
|
||||
cond do
|
||||
real_capacity && real_capacity <= 0 ->
|
||||
{:error, field: :real_capacity, message: "must be greater than 0"}
|
||||
|
||||
public_capacity && public_capacity <= 0 ->
|
||||
{:error, field: :public_capacity, message: "must be greater than 0"}
|
||||
|
||||
real_capacity && public_capacity && public_capacity > real_capacity ->
|
||||
{:error,
|
||||
field: :public_capacity, message: "must be less than or equal to real_capacity"}
|
||||
|
||||
true ->
|
||||
:ok
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
attributes do
|
||||
|
|
@ -22,6 +46,8 @@ defmodule SpazioSolazzo.BookingSystem.Space do
|
|||
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
|
||||
|
|
|
|||
|
|
@ -20,6 +20,18 @@ defmodule SpazioSolazzo.BookingSystem.TimeSlotTemplate do
|
|||
|
||||
create :create do
|
||||
accept [:start_time, :end_time, :space_id, :day_of_week]
|
||||
|
||||
validate fn changeset, _ctx ->
|
||||
start_time = Ash.Changeset.get_attribute(changeset, :start_time)
|
||||
end_time = Ash.Changeset.get_attribute(changeset, :end_time)
|
||||
|
||||
if start_time && end_time && Time.compare(end_time, start_time) != :gt do
|
||||
{:error, field: :end_time, message: "must be after start time"}
|
||||
else
|
||||
:ok
|
||||
end
|
||||
end
|
||||
|
||||
change {Changes.PreventCreationOverlap, []}
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -18,28 +18,33 @@ defmodule SpazioSolazzo.BookingSystem.TimeSlotTemplate.Changes.PreventCreationOv
|
|||
end_time = Ash.Changeset.get_attribute(changeset, :end_time)
|
||||
day_of_week = Ash.Changeset.get_attribute(changeset, :day_of_week)
|
||||
|
||||
overlapping =
|
||||
TimeSlotTemplate
|
||||
|> Ash.Query.filter(space_id == ^space_id)
|
||||
|> Ash.Query.filter(day_of_week == ^day_of_week)
|
||||
|> Ash.Query.filter(start_time < ^end_time and end_time > ^start_time)
|
||||
|> Ash.read()
|
||||
# Skip overlap check if essential attributes are missing
|
||||
if is_nil(space_id) or is_nil(start_time) or is_nil(end_time) or is_nil(day_of_week) do
|
||||
changeset
|
||||
else
|
||||
overlapping =
|
||||
TimeSlotTemplate
|
||||
|> Ash.Query.filter(space_id == ^space_id)
|
||||
|> Ash.Query.filter(day_of_week == ^day_of_week)
|
||||
|> Ash.Query.filter(start_time < ^end_time and end_time > ^start_time)
|
||||
|> Ash.read()
|
||||
|
||||
case overlapping do
|
||||
{:ok, []} ->
|
||||
changeset
|
||||
case overlapping do
|
||||
{:ok, []} ->
|
||||
changeset
|
||||
|
||||
{:ok, _} ->
|
||||
Changeset.add_error(changeset,
|
||||
field: :base,
|
||||
message: "time slot overlaps with existing template for this space and day"
|
||||
)
|
||||
{:ok, _} ->
|
||||
Changeset.add_error(changeset,
|
||||
field: :base,
|
||||
message: "overlaps with existing time slot"
|
||||
)
|
||||
|
||||
{:error, err} ->
|
||||
Changeset.add_error(changeset,
|
||||
field: :base,
|
||||
message: "failed to validate overlap: #{inspect(err)}"
|
||||
)
|
||||
{:error, err} ->
|
||||
Changeset.add_error(changeset,
|
||||
field: :base,
|
||||
message: "failed to validate overlap: #{inspect(err)}"
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -94,6 +94,8 @@ defmodule SpazioSolazzoWeb do
|
|||
import Phoenix.HTML
|
||||
# Core UI components
|
||||
import SpazioSolazzoWeb.CoreComponents
|
||||
# Landing page components
|
||||
import SpazioSolazzoWeb.LandingComponents
|
||||
|
||||
# Common modules used in templates
|
||||
alias Phoenix.LiveView.JS
|
||||
|
|
|
|||
|
|
@ -1,77 +1,12 @@
|
|||
defmodule SpazioSolazzoWeb.BookingController do
|
||||
use SpazioSolazzoWeb, :controller
|
||||
|
||||
alias SpazioSolazzo.BookingSystem
|
||||
alias SpazioSolazzo.BookingSystem.Booking
|
||||
alias SpazioSolazzo.BookingSystem.Booking.Token
|
||||
|
||||
def confirm(conn, %{"token" => token}) do
|
||||
case Token.verify(token) do
|
||||
{:ok, %{booking_id: booking_id, role: :admin, action: :confirm}} ->
|
||||
case Ash.get(Booking, booking_id, error?: false) do
|
||||
{:ok, nil} ->
|
||||
conn
|
||||
|> put_flash(:error, "Booking not found, cancelling aborted.")
|
||||
|> redirect(to: "/")
|
||||
|
||||
{:ok, booking} ->
|
||||
action_result = BookingSystem.confirm_booking(booking)
|
||||
build_response(conn, action_result, :confirm)
|
||||
|
||||
{:error, _} ->
|
||||
conn
|
||||
|> put_flash(:error, "Unexpected error occurred, couldn't cancel booking.")
|
||||
|> redirect(to: "/")
|
||||
end
|
||||
|
||||
_ ->
|
||||
conn
|
||||
|> put_flash(:error, "Invalid or expired link.")
|
||||
|> redirect(to: "/")
|
||||
end
|
||||
def confirm(conn, %{"token" => _token}) do
|
||||
conn
|
||||
|> put_flash(
|
||||
:info,
|
||||
"Please use the admin dashboard to manage booking requests."
|
||||
)
|
||||
|> redirect(to: "/admin/dashboard")
|
||||
end
|
||||
|
||||
def cancel(conn, %{"token" => token}) do
|
||||
case Token.verify(token) do
|
||||
{:ok, %{booking_id: booking_id, role: _, action: :cancel}} ->
|
||||
case Ash.get(Booking, booking_id, error?: false) do
|
||||
{:ok, nil} ->
|
||||
conn
|
||||
|> put_flash(:error, "Booking not found, cancelling aborted.")
|
||||
|> redirect(to: "/")
|
||||
|
||||
{:ok, booking} ->
|
||||
action_result = BookingSystem.cancel_booking(booking)
|
||||
build_response(conn, action_result, :cancel)
|
||||
|
||||
{:error, _} ->
|
||||
conn
|
||||
|> put_flash(:error, "Unexpected error occurred, couldn't cancel booking.")
|
||||
|> redirect(to: "/")
|
||||
end
|
||||
|
||||
_ ->
|
||||
conn
|
||||
|> put_flash(:error, "Invalid or expired link.")
|
||||
|> redirect(to: "/")
|
||||
end
|
||||
end
|
||||
|
||||
defp build_response(conn, action_result, action_name) do
|
||||
case action_result do
|
||||
{:ok, _booking} ->
|
||||
conn
|
||||
|> put_flash(:info, success_message(action_name))
|
||||
|> redirect(to: "/")
|
||||
|
||||
{:error, _} ->
|
||||
conn
|
||||
|> put_flash(:error, "Action could not be completed (e.g. already processed).")
|
||||
|> redirect(to: "/")
|
||||
end
|
||||
end
|
||||
|
||||
defp success_message(:cancel), do: "The booking has been cancelled."
|
||||
defp success_message(:confirm), do: "The booking has been confirmed."
|
||||
defp success_message(_), do: "Action completed successfully."
|
||||
end
|
||||
|
|
|
|||
|
|
@ -1,18 +1,26 @@
|
|||
<h1 class="text-orange">🎉 Booking Confirmed!</h1>
|
||||
<h1 class="text-orange">🎉 Booking Request Approved!</h1>
|
||||
|
||||
<p>Hello <strong><%= @customer_name %></strong>,</p>
|
||||
<p>Thank you for choosing Spazio Solazzo! Your booking has been successfully confirmed.</p>
|
||||
<p>
|
||||
Great news! Your booking request has been approved. We look forward to seeing you at Spazio Solazzo!
|
||||
</p>
|
||||
|
||||
<.details_list>
|
||||
<.detail_item label="Date">{@date}</.detail_item>
|
||||
<.detail_item label="Time">{@start_time} - {@end_time}</.detail_item>
|
||||
<.detail_item label="Space">{@space_name}</.detail_item>
|
||||
<.detail_item label="Email">{@customer_email}</.detail_item>
|
||||
<.detail_item label="Phone">{@customer_phone || "N/A"}</.detail_item>
|
||||
<.detail_item label="Note">{@customer_comment || "N/A"}</.detail_item>
|
||||
</.details_list>
|
||||
|
||||
<div style="background-color: #f0fdf4; border-left: 4px solid #22c55e; padding: 15px; margin: 20px 0; color: #2d3748;">
|
||||
<p style="margin: 0; font-weight: 500;">
|
||||
✅ Your booking is confirmed! Please arrive on time.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<p class="text-center">
|
||||
If you need to manage or cancel this booking, please use the link below:
|
||||
If you need to cancel this booking, please use the link below:
|
||||
</p>
|
||||
|
||||
<.email_button href={@cancel_url} variant={:danger}>
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
<h1 style="color: #dc3545;">🚫 Booking Request Cancelled</h1>
|
||||
|
||||
<p>A booking request has been cancelled by the customer.</p>
|
||||
|
||||
<div style="margin-bottom: 20px;">
|
||||
<p><strong>Customer:</strong> {@customer_name}</p>
|
||||
<p><strong>Email:</strong> <a href={"mailto:#{@customer_email}"}>{@customer_email}</a></p>
|
||||
|
||||
<%= if @customer_phone && String.trim(@customer_phone) != "" do %>
|
||||
<p><strong>Phone:</strong> <a href={"tel:#{@customer_phone}"}>{@customer_phone}</a></p>
|
||||
<% else %>
|
||||
<p><strong>Phone:</strong> N/A</p>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<.details_list>
|
||||
<.detail_item label="Space">{@space_name}</.detail_item>
|
||||
<.detail_item label="Date">{@date}</.detail_item>
|
||||
<.detail_item label="Time">{@start_time} - {@end_time}</.detail_item>
|
||||
</.details_list>
|
||||
|
||||
<div style="background-color: #fff5f5; border-left: 4px solid #dc3545; padding: 15px; margin: 20px 0; color: #2d3748;">
|
||||
<h3 style="color: #dc3545; margin-top: 0; font-size: 16px;">Cancellation Reason:</h3>
|
||||
<p style="margin: 0; font-weight: 500;">
|
||||
{@cancellation_reason}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<p style="color: #718096; font-size: 14px; font-style: italic;">
|
||||
This is an automated notification. No action is required.
|
||||
</p>
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
<h1 class="text-orange">🔔 New Booking Received</h1>
|
||||
<h1 class="text-orange">🔔 New Booking Request</h1>
|
||||
|
||||
<div style="margin-bottom: 20px;">
|
||||
<p><strong>Customer:</strong> {@customer_name}</p>
|
||||
|
|
@ -7,11 +7,12 @@
|
|||
<%= if @customer_phone && String.trim(@customer_phone) != "" do %>
|
||||
<p><strong>Phone:</strong> <a href={"tel:#{@customer_phone}"}>{@customer_phone}</a></p>
|
||||
<% else %>
|
||||
<p><strong>Phone:</strong> <a href="#">N/A</a></p>
|
||||
<p><strong>Phone:</strong> N/A</p>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<.details_list>
|
||||
<.detail_item label="Space">{@space_name}</.detail_item>
|
||||
<.detail_item label="Date">{@date}</.detail_item>
|
||||
<.detail_item label="Time">{@start_time} - {@end_time}</.detail_item>
|
||||
</.details_list>
|
||||
|
|
@ -21,7 +22,7 @@
|
|||
|
||||
<%= if @customer_comment && String.trim(@customer_comment) != "" do %>
|
||||
<div style="background-color: #fffaf0; border-left: 4px solid #ed8936; padding: 15px; color: #2d3748; font-size: 16px; font-weight: 500;">
|
||||
“{@customer_comment}”
|
||||
"{@customer_comment}"
|
||||
</div>
|
||||
<% else %>
|
||||
<div style="background-color: #f7fafc; padding: 12px; border-radius: 4px; color: #718096; font-style: italic; border: 1px dashed #cbd5e0;">
|
||||
|
|
@ -32,13 +33,9 @@
|
|||
|
||||
<hr class="divider" />
|
||||
|
||||
<h3 style="color: #5C6BC0; text-align: center;">Admin Actions</h3>
|
||||
<p class="text-center">Please confirm arrival or cancel the booking.</p>
|
||||
<h3 style="color: #5C6BC0; text-align: center;">Action Required</h3>
|
||||
<p class="text-center">Please review and manage this request in the admin dashboard.</p>
|
||||
|
||||
<.email_button href={@confirm_url} variant={:primary}>
|
||||
Confirm Arrival
|
||||
</.email_button>
|
||||
|
||||
<.email_button href={@cancel_url} variant={:danger}>
|
||||
Cancel Booking
|
||||
<.email_button href={@dashboard_url} variant={:primary}>
|
||||
Manage Request
|
||||
</.email_button>
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
<h1 style="color: #dc3545;">❌ Booking Request Not Approved</h1>
|
||||
|
||||
<p>Hello <strong><%= @customer_name %></strong>,</p>
|
||||
<p>We regret to inform you that your booking request could not be approved at this time.</p>
|
||||
|
||||
<.details_list>
|
||||
<.detail_item label="Date">{@date}</.detail_item>
|
||||
<.detail_item label="Time">{@start_time} - {@end_time}</.detail_item>
|
||||
<.detail_item label="Space">{@space_name}</.detail_item>
|
||||
</.details_list>
|
||||
|
||||
<div style="background-color: #fff5f5; border-left: 4px solid #dc3545; padding: 15px; margin: 20px 0; color: #2d3748;">
|
||||
<h3 style="color: #dc3545; margin-top: 0; font-size: 16px;">Reason for Rejection:</h3>
|
||||
<p style="margin: 0; font-weight: 500;">
|
||||
{@rejection_reason}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<p>
|
||||
We apologize for any inconvenience. If you have any questions or would like to discuss alternative options, please don't hesitate to contact us.
|
||||
</p>
|
||||
|
||||
<hr class="divider" />
|
||||
|
||||
<div style="background-color: #f8fafc; border-radius: 8px; padding: 20px; text-align: center; margin-top: 30px;">
|
||||
<h3 style="color: #2d3748; margin-top: 0;">Get in Touch</h3>
|
||||
<p style="color: #4a5568; font-size: 14px; margin-bottom: 15px;">
|
||||
Our Front Office is here to help you find the best solution.
|
||||
</p>
|
||||
|
||||
<a
|
||||
href={"tel:#{@front_office_phone_number}"}
|
||||
style="display: inline-block; background-color: #edf2f7; color: #2d3748; padding: 10px 20px; border-radius: 50px; text-decoration: none; font-weight: bold; font-size: 18px; border: 1px solid #cbd5e0;"
|
||||
>
|
||||
📞 {@front_office_phone_number}
|
||||
</a>
|
||||
</div>
|
||||
|
|
@ -0,0 +1,46 @@
|
|||
<h1 class="text-orange">✅ Request Received!</h1>
|
||||
|
||||
<p>Hello <strong><%= @customer_name %></strong>,</p>
|
||||
<p>
|
||||
Thank you for choosing Spazio Solazzo! Your booking request has been received and is pending approval.
|
||||
</p>
|
||||
|
||||
<.details_list>
|
||||
<.detail_item label="Date">{@date}</.detail_item>
|
||||
<.detail_item label="Time">{@start_time} - {@end_time}</.detail_item>
|
||||
<.detail_item label="Space">{@space_name}</.detail_item>
|
||||
<.detail_item label="Email">{@customer_email}</.detail_item>
|
||||
<.detail_item label="Phone">{@customer_phone || "N/A"}</.detail_item>
|
||||
<.detail_item label="Note">{@customer_comment || "N/A"}</.detail_item>
|
||||
</.details_list>
|
||||
|
||||
<div style="background-color: #fffaf0; border-left: 4px solid #ed8936; padding: 15px; margin: 20px 0; color: #2d3748;">
|
||||
<p style="margin: 0; font-weight: 500;">
|
||||
⏳ Your request is pending approval. You will receive an email once an administrator reviews your request.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<p class="text-center">
|
||||
If you need to cancel this request, please use the link below:
|
||||
</p>
|
||||
|
||||
<.email_button href={@cancel_url} variant={:danger}>
|
||||
Cancel Request
|
||||
</.email_button>
|
||||
|
||||
<hr class="divider" />
|
||||
|
||||
<div style="background-color: #f8fafc; border-radius: 8px; padding: 20px; text-align: center; margin-top: 30px;">
|
||||
<h3 style="color: #2d3748; margin-top: 0;">Need Help?</h3>
|
||||
<p style="color: #4a5568; font-size: 14px; margin-bottom: 15px;">
|
||||
Do you have questions or need to update your request details? <br />
|
||||
Our Front Office is available to assist you at any time.
|
||||
</p>
|
||||
|
||||
<a
|
||||
href={"tel:#{@front_office_phone_number}"}
|
||||
style="display: inline-block; background-color: #edf2f7; color: #2d3748; padding: 10px 20px; border-radius: 50px; text-decoration: none; font-weight: bold; font-size: 18px; border: 1px solid #cbd5e0;"
|
||||
>
|
||||
📞 {@front_office_phone_number}
|
||||
</a>
|
||||
</div>
|
||||
|
|
@ -1,21 +1,242 @@
|
|||
defmodule SpazioSolazzoWeb.Admin.DashboardLive do
|
||||
@moduledoc """
|
||||
Admin dashboard home page. Lists the available tools that admins have
|
||||
Admin dashboard for managing booking requests and creating walk-in bookings.
|
||||
"""
|
||||
|
||||
use SpazioSolazzoWeb, :live_view
|
||||
|
||||
alias SpazioSolazzo.BookingSystem
|
||||
import SpazioSolazzoWeb.AdminComponents
|
||||
|
||||
def mount(_params, _session, socket) do
|
||||
{:ok, coworking_space} = BookingSystem.get_space_by_slug("coworking", not_found_error?: false)
|
||||
{:ok, meeting_space} = BookingSystem.get_space_by_slug("meeting", not_found_error?: false)
|
||||
{:ok, spaces} = Ash.read(SpazioSolazzo.BookingSystem.Space)
|
||||
{:ok, requests} = BookingSystem.list_booking_requests(nil, nil, nil, load: [:space, :user])
|
||||
|
||||
if connected?(socket) do
|
||||
Phoenix.PubSub.subscribe(SpazioSolazzo.PubSub, "booking:created")
|
||||
end
|
||||
|
||||
{:ok,
|
||||
assign(socket,
|
||||
coworking_space: coworking_space,
|
||||
meeting_space: meeting_space
|
||||
active_tab: :requests,
|
||||
spaces: spaces,
|
||||
requests: requests,
|
||||
filter_space_id: nil,
|
||||
filter_email: nil,
|
||||
filter_date: nil,
|
||||
show_reject_modal: false,
|
||||
rejecting_booking_id: nil,
|
||||
rejection_reason: "",
|
||||
walk_in_form: %{
|
||||
space_id: nil,
|
||||
customer_name: "",
|
||||
customer_email: "",
|
||||
customer_phone: "",
|
||||
customer_comment: "",
|
||||
start_datetime: nil,
|
||||
end_datetime: nil
|
||||
},
|
||||
capacity_warning: nil
|
||||
)}
|
||||
end
|
||||
|
||||
def handle_event("switch_tab", %{"tab" => tab}, socket) do
|
||||
{:noreply, assign(socket, active_tab: String.to_existing_atom(tab))}
|
||||
end
|
||||
|
||||
def handle_event("filter_requests", params, socket) do
|
||||
space_id = if params["space_id"] == "", do: nil, else: params["space_id"]
|
||||
email = if params["email"] == "", do: nil, else: params["email"]
|
||||
|
||||
date =
|
||||
if params["date"] == "",
|
||||
do: nil,
|
||||
else: Date.from_iso8601!(params["date"])
|
||||
|
||||
{:ok, requests} =
|
||||
BookingSystem.list_booking_requests(space_id, email, date, load: [:space, :user])
|
||||
|
||||
{:noreply,
|
||||
assign(socket,
|
||||
requests: requests,
|
||||
filter_space_id: space_id,
|
||||
filter_email: email,
|
||||
filter_date: date
|
||||
)}
|
||||
end
|
||||
|
||||
def handle_event("approve_booking", %{"booking_id" => booking_id}, socket) do
|
||||
case Ash.get(SpazioSolazzo.BookingSystem.Booking, booking_id) do
|
||||
{:ok, booking} ->
|
||||
case BookingSystem.approve_booking(booking) do
|
||||
{:ok, _approved} ->
|
||||
{:ok, requests} =
|
||||
BookingSystem.list_booking_requests(
|
||||
socket.assigns.filter_space_id,
|
||||
socket.assigns.filter_email,
|
||||
socket.assigns.filter_date,
|
||||
load: [:space, :user]
|
||||
)
|
||||
|
||||
{:noreply,
|
||||
socket
|
||||
|> assign(requests: requests)
|
||||
|> put_flash(:info, "Booking approved successfully")}
|
||||
|
||||
{:error, _} ->
|
||||
{:noreply, put_flash(socket, :error, "Failed to approve booking")}
|
||||
end
|
||||
|
||||
{:error, _} ->
|
||||
{:noreply, put_flash(socket, :error, "Booking not found")}
|
||||
end
|
||||
end
|
||||
|
||||
def handle_event("show_reject_modal", %{"booking_id" => booking_id}, socket) do
|
||||
{:noreply,
|
||||
assign(socket,
|
||||
show_reject_modal: true,
|
||||
rejecting_booking_id: booking_id,
|
||||
rejection_reason: ""
|
||||
)}
|
||||
end
|
||||
|
||||
def handle_event("hide_reject_modal", _, socket) do
|
||||
{:noreply,
|
||||
assign(socket,
|
||||
show_reject_modal: false,
|
||||
rejecting_booking_id: nil,
|
||||
rejection_reason: ""
|
||||
)}
|
||||
end
|
||||
|
||||
def handle_event("update_rejection_reason", %{"reason" => reason}, socket) do
|
||||
{:noreply, assign(socket, rejection_reason: reason)}
|
||||
end
|
||||
|
||||
def handle_event("confirm_reject", _, socket) do
|
||||
if String.trim(socket.assigns.rejection_reason) == "" do
|
||||
{:noreply, put_flash(socket, :error, "Please provide a rejection reason")}
|
||||
else
|
||||
case Ash.get(SpazioSolazzo.BookingSystem.Booking, socket.assigns.rejecting_booking_id) do
|
||||
{:ok, booking} ->
|
||||
case BookingSystem.reject_booking(booking, socket.assigns.rejection_reason) do
|
||||
{:ok, _rejected} ->
|
||||
{:ok, requests} =
|
||||
BookingSystem.list_booking_requests(
|
||||
socket.assigns.filter_space_id,
|
||||
socket.assigns.filter_email,
|
||||
socket.assigns.filter_date,
|
||||
load: [:space, :user]
|
||||
)
|
||||
|
||||
{:noreply,
|
||||
socket
|
||||
|> assign(
|
||||
requests: requests,
|
||||
show_reject_modal: false,
|
||||
rejecting_booking_id: nil,
|
||||
rejection_reason: ""
|
||||
)
|
||||
|> put_flash(:info, "Booking rejected")}
|
||||
|
||||
{:error, _} ->
|
||||
{:noreply, put_flash(socket, :error, "Failed to reject booking")}
|
||||
end
|
||||
|
||||
{:error, _} ->
|
||||
{:noreply, put_flash(socket, :error, "Booking not found")}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def handle_event("update_walk_in_form", params, socket) do
|
||||
form = socket.assigns.walk_in_form
|
||||
|
||||
updated_form =
|
||||
Map.merge(form, %{
|
||||
space_id: params["space_id"],
|
||||
customer_name: params["customer_name"] || form.customer_name,
|
||||
customer_email: params["customer_email"] || form.customer_email,
|
||||
customer_phone: params["customer_phone"] || form.customer_phone,
|
||||
customer_comment: params["customer_comment"] || form.customer_comment,
|
||||
start_datetime: parse_datetime(params["start_datetime"]),
|
||||
end_datetime: parse_datetime(params["end_datetime"])
|
||||
})
|
||||
|
||||
{:noreply, assign(socket, walk_in_form: updated_form)}
|
||||
end
|
||||
|
||||
def handle_event("create_walk_in", _, socket) do
|
||||
form = socket.assigns.walk_in_form
|
||||
|
||||
with true <- form.space_id != nil,
|
||||
true <- form.customer_name != "",
|
||||
true <- form.customer_email != "",
|
||||
true <- form.start_datetime != nil,
|
||||
true <- form.end_datetime != nil do
|
||||
case BookingSystem.create_walk_in(
|
||||
form.space_id,
|
||||
%{
|
||||
name: form.customer_name,
|
||||
email: form.customer_email,
|
||||
phone: form.customer_phone,
|
||||
comment: form.customer_comment
|
||||
},
|
||||
form.start_datetime,
|
||||
form.end_datetime
|
||||
) do
|
||||
{:ok, _booking} ->
|
||||
{:noreply,
|
||||
socket
|
||||
|> assign(
|
||||
walk_in_form: %{
|
||||
space_id: nil,
|
||||
customer_name: "",
|
||||
customer_email: "",
|
||||
customer_phone: "",
|
||||
customer_comment: "",
|
||||
start_datetime: nil,
|
||||
end_datetime: nil
|
||||
},
|
||||
capacity_warning: nil
|
||||
)
|
||||
|> put_flash(:info, "Walk-in booking created successfully")}
|
||||
|
||||
{:error, error} ->
|
||||
{:noreply, put_flash(socket, :error, "Failed to create walk-in: #{inspect(error)}")}
|
||||
end
|
||||
else
|
||||
_ ->
|
||||
{:noreply, put_flash(socket, :error, "Please fill in all required fields")}
|
||||
end
|
||||
end
|
||||
|
||||
def handle_info(
|
||||
%{topic: "booking:created", payload: %{data: _data}},
|
||||
socket
|
||||
) do
|
||||
{:ok, requests} =
|
||||
BookingSystem.list_booking_requests(
|
||||
socket.assigns.filter_space_id,
|
||||
socket.assigns.filter_email,
|
||||
socket.assigns.filter_date,
|
||||
load: [:space, :user]
|
||||
)
|
||||
|
||||
{:noreply, assign(socket, requests: requests)}
|
||||
end
|
||||
|
||||
def handle_info(_msg, socket) do
|
||||
{:noreply, socket}
|
||||
end
|
||||
|
||||
defp parse_datetime(nil), do: nil
|
||||
defp parse_datetime(""), do: nil
|
||||
|
||||
defp parse_datetime(datetime_string) do
|
||||
case DateTime.from_iso8601(datetime_string <> ":00Z") do
|
||||
{:ok, dt, _} -> dt
|
||||
_ -> nil
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -1,28 +1,328 @@
|
|||
<Layouts.app flash={@flash} current_user={@current_user}>
|
||||
<div class="mx-auto max-w-[1200px] px-6 py-12">
|
||||
<.back_to_link
|
||||
navigate={~p"/"}
|
||||
value="Back to Home"
|
||||
/>
|
||||
<div class="min-h-screen bg-gradient-to-br from-slate-50 to-slate-100 py-8 px-4 sm:px-6 lg:px-8">
|
||||
<div class="max-w-7xl mx-auto">
|
||||
<div class="text-center mb-8">
|
||||
<h1 class="text-4xl font-bold text-slate-900 mb-2">Admin Dashboard</h1>
|
||||
<p class="text-lg text-slate-600">Manage booking requests and walk-in customers</p>
|
||||
</div>
|
||||
|
||||
<h1 class="text-3xl text-base-content font-bold mb-8">Admin Dashboard</h1>
|
||||
<%!-- Tab Navigation --%>
|
||||
<div class="bg-white rounded-t-2xl shadow-lg">
|
||||
<div class="border-b border-slate-200">
|
||||
<nav class="flex space-x-8 px-8 pt-6" aria-label="Tabs">
|
||||
<button
|
||||
phx-click="switch_tab"
|
||||
phx-value-tab="requests"
|
||||
class={[
|
||||
"pb-4 px-1 border-b-2 font-semibold text-sm transition-colors",
|
||||
if(@active_tab == :requests,
|
||||
do: "border-orange-500 text-orange-600",
|
||||
else:
|
||||
"border-transparent text-slate-500 hover:text-slate-700 hover:border-slate-300"
|
||||
)
|
||||
]}
|
||||
>
|
||||
Booking Requests
|
||||
<%= if length(@requests) > 0 do %>
|
||||
<span class="ml-2 bg-orange-100 text-orange-800 py-0.5 px-2.5 rounded-full text-xs font-medium">
|
||||
{length(@requests)}
|
||||
</span>
|
||||
<% end %>
|
||||
</button>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
<%= if @meeting_space do %>
|
||||
<.tool_card
|
||||
title={@meeting_space.name}
|
||||
description="Create walk-in bookings for the space"
|
||||
icon="hero-user-group"
|
||||
/>
|
||||
<% end %>
|
||||
<button
|
||||
phx-click="switch_tab"
|
||||
phx-value-tab="walk_in"
|
||||
class={[
|
||||
"pb-4 px-1 border-b-2 font-semibold text-sm transition-colors",
|
||||
if(@active_tab == :walk_in,
|
||||
do: "border-orange-500 text-orange-600",
|
||||
else:
|
||||
"border-transparent text-slate-500 hover:text-slate-700 hover:border-slate-300"
|
||||
)
|
||||
]}
|
||||
>
|
||||
Walk-in / Custom
|
||||
</button>
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
<%= if @coworking_space do %>
|
||||
<.tool_card
|
||||
title={@coworking_space.name}
|
||||
description="Create walk-in bookings for the space"
|
||||
icon="hero-user-group"
|
||||
/>
|
||||
<% end %>
|
||||
<div class="p-8">
|
||||
<%= if @active_tab == :requests do %>
|
||||
<%!-- Request Manager Tab --%>
|
||||
<div class="space-y-6">
|
||||
<%!-- Filters --%>
|
||||
<form phx-change="filter_requests" class="bg-slate-50 rounded-xl p-6">
|
||||
<h3 class="text-lg font-semibold text-slate-900 mb-4">Filter Requests</h3>
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-slate-700 mb-2">Space</label>
|
||||
<select
|
||||
name="space_id"
|
||||
class="w-full px-4 py-2 border border-slate-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-orange-500"
|
||||
>
|
||||
<option value="">All Spaces</option>
|
||||
<%= for space <- @spaces do %>
|
||||
<option value={space.id} selected={@filter_space_id == space.id}>
|
||||
{space.name}
|
||||
</option>
|
||||
<% end %>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-slate-700 mb-2">Email</label>
|
||||
<input
|
||||
type="email"
|
||||
name="email"
|
||||
value={@filter_email || ""}
|
||||
placeholder="customer@example.com"
|
||||
class="w-full px-4 py-2 border border-slate-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-orange-500"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-slate-700 mb-2">Date</label>
|
||||
<input
|
||||
type="date"
|
||||
name="date"
|
||||
value={if @filter_date, do: Date.to_iso8601(@filter_date), else: ""}
|
||||
class="w-full px-4 py-2 border border-slate-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-orange-500"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<%!-- Requests List --%>
|
||||
<%= if @requests == [] do %>
|
||||
<div class="text-center py-12 bg-slate-50 rounded-xl">
|
||||
<p class="text-slate-500 text-lg">No pending requests</p>
|
||||
</div>
|
||||
<% else %>
|
||||
<div class="space-y-4">
|
||||
<%= for request <- @requests do %>
|
||||
<div class="bg-white border border-slate-200 rounded-xl p-6 hover:shadow-md transition-shadow">
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div>
|
||||
<h4 class="text-lg font-semibold text-slate-900 mb-3">
|
||||
{request.customer_name}
|
||||
</h4>
|
||||
<div class="space-y-2 text-sm text-slate-600">
|
||||
<p>
|
||||
<strong>Email:</strong>
|
||||
<a
|
||||
href={"mailto:#{request.customer_email}"}
|
||||
class="text-orange-600 hover:underline"
|
||||
>
|
||||
{request.customer_email}
|
||||
</a>
|
||||
</p>
|
||||
<%= if request.customer_phone do %>
|
||||
<p>
|
||||
<strong>Phone:</strong>
|
||||
<a
|
||||
href={"tel:#{request.customer_phone}"}
|
||||
class="text-orange-600 hover:underline"
|
||||
>
|
||||
{request.customer_phone}
|
||||
</a>
|
||||
</p>
|
||||
<% end %>
|
||||
<p>
|
||||
<strong>Space:</strong>
|
||||
{request.space.name}
|
||||
</p>
|
||||
<p>
|
||||
<strong>Date:</strong>
|
||||
{Calendar.strftime(request.date, "%A, %B %d, %Y")}
|
||||
</p>
|
||||
<p>
|
||||
<strong>Time:</strong>
|
||||
{request.start_time} - {request.end_time}
|
||||
</p>
|
||||
<%= if request.customer_comment do %>
|
||||
<div class="mt-3 p-3 bg-orange-50 rounded-lg">
|
||||
<p class="text-sm font-medium text-slate-700">Comment:</p>
|
||||
<p class="text-sm text-slate-600 italic">
|
||||
{request.customer_comment}
|
||||
</p>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col justify-center space-y-3">
|
||||
<button
|
||||
phx-click="approve_booking"
|
||||
phx-value-booking_id={request.id}
|
||||
class="w-full bg-green-500 text-white px-6 py-3 rounded-xl font-semibold hover:bg-green-600 transition-colors"
|
||||
>
|
||||
Approve
|
||||
</button>
|
||||
<button
|
||||
phx-click="show_reject_modal"
|
||||
phx-value-booking_id={request.id}
|
||||
class="w-full bg-red-500 text-white px-6 py-3 rounded-xl font-semibold hover:bg-red-600 transition-colors"
|
||||
>
|
||||
Reject
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
<% else %>
|
||||
<%!-- Walk-in Tab --%>
|
||||
<div>
|
||||
<h3 class="text-2xl font-bold text-slate-900 mb-6">Create Walk-in Booking</h3>
|
||||
|
||||
<form phx-change="update_walk_in_form" phx-submit="create_walk_in" class="space-y-6">
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div>
|
||||
<label class="block text-sm font-semibold text-slate-900 mb-2">
|
||||
Space <span class="text-red-500">*</span>
|
||||
</label>
|
||||
<select
|
||||
name="space_id"
|
||||
required
|
||||
class="w-full px-4 py-3 border border-slate-300 rounded-xl focus:outline-none focus:ring-2 focus:ring-orange-500"
|
||||
>
|
||||
<option value="">Select a space</option>
|
||||
<%= for space <- @spaces do %>
|
||||
<option value={space.id}>{space.name}</option>
|
||||
<% end %>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-semibold text-slate-900 mb-2">
|
||||
Customer Name <span class="text-red-500">*</span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
name="customer_name"
|
||||
required
|
||||
placeholder="John Doe"
|
||||
class="w-full px-4 py-3 border border-slate-300 rounded-xl focus:outline-none focus:ring-2 focus:ring-orange-500"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-semibold text-slate-900 mb-2">
|
||||
Email <span class="text-red-500">*</span>
|
||||
</label>
|
||||
<input
|
||||
type="email"
|
||||
name="customer_email"
|
||||
required
|
||||
placeholder="john@example.com"
|
||||
class="w-full px-4 py-3 border border-slate-300 rounded-xl focus:outline-none focus:ring-2 focus:ring-orange-500"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-semibold text-slate-900 mb-2">Phone</label>
|
||||
<input
|
||||
type="tel"
|
||||
name="customer_phone"
|
||||
placeholder="+39 123 456 7890"
|
||||
class="w-full px-4 py-3 border border-slate-300 rounded-xl focus:outline-none focus:ring-2 focus:ring-orange-500"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-semibold text-slate-900 mb-2">
|
||||
Start Date & Time <span class="text-red-500">*</span>
|
||||
</label>
|
||||
<input
|
||||
type="datetime-local"
|
||||
name="start_datetime"
|
||||
required
|
||||
class="w-full px-4 py-3 border border-slate-300 rounded-xl focus:outline-none focus:ring-2 focus:ring-orange-500"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-semibold text-slate-900 mb-2">
|
||||
End Date & Time <span class="text-red-500">*</span>
|
||||
</label>
|
||||
<input
|
||||
type="datetime-local"
|
||||
name="end_datetime"
|
||||
required
|
||||
class="w-full px-4 py-3 border border-slate-300 rounded-xl focus:outline-none focus:ring-2 focus:ring-orange-500"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-semibold text-slate-900 mb-2">Comment</label>
|
||||
<textarea
|
||||
name="customer_comment"
|
||||
rows="3"
|
||||
placeholder="Any additional notes..."
|
||||
class="w-full px-4 py-3 border border-slate-300 rounded-xl focus:outline-none focus:ring-2 focus:ring-orange-500"
|
||||
></textarea>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
class="w-full md:w-auto bg-orange-500 text-white px-8 py-3 rounded-xl font-semibold hover:bg-orange-600 transition-colors"
|
||||
>
|
||||
Create Walk-in Booking
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<%!-- 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">
|
||||
<div class="bg-white rounded-2xl shadow-2xl max-w-md w-full p-8">
|
||||
<h3 class="text-2xl font-bold text-slate-900 mb-4">Reject Booking</h3>
|
||||
|
||||
<p class="text-slate-600 mb-6">
|
||||
Please provide a reason for rejecting this booking request. The customer will receive this reason in their email.
|
||||
</p>
|
||||
|
||||
<form phx-submit="confirm_reject">
|
||||
<div class="mb-6">
|
||||
<label class="block text-sm font-semibold text-slate-900 mb-2">
|
||||
Rejection Reason <span class="text-red-500">*</span>
|
||||
</label>
|
||||
<textarea
|
||||
phx-change="update_rejection_reason"
|
||||
name="reason"
|
||||
rows="4"
|
||||
required
|
||||
placeholder="e.g., Space under maintenance, Fully booked, etc."
|
||||
class="w-full px-4 py-3 border border-slate-300 rounded-xl focus:outline-none focus:ring-2 focus:ring-orange-500"
|
||||
>{@rejection_reason}</textarea>
|
||||
</div>
|
||||
|
||||
<div class="flex gap-3">
|
||||
<button
|
||||
type="button"
|
||||
phx-click="hide_reject_modal"
|
||||
class="flex-1 bg-slate-200 text-slate-700 px-6 py-3 rounded-xl font-semibold hover:bg-slate-300 transition-colors"
|
||||
>
|
||||
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"
|
||||
>
|
||||
Confirm Rejection
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
</Layouts.app>
|
||||
|
|
|
|||
|
|
@ -1,130 +1 @@
|
|||
defmodule SpazioSolazzoWeb.AssetBookingLive do
|
||||
use SpazioSolazzoWeb, :live_view
|
||||
|
||||
alias SpazioSolazzo.BookingSystem
|
||||
import SpazioSolazzoWeb.BookingComponents
|
||||
require Ash.Query
|
||||
|
||||
def mount(%{"asset_id" => asset_id}, _session, socket) do
|
||||
case BookingSystem.get_asset_by_id(asset_id, load: [:space]) do
|
||||
{:ok, asset} ->
|
||||
selected_date = Date.utc_today()
|
||||
|
||||
{:ok, time_slots} =
|
||||
BookingSystem.get_space_time_slots_by_date(asset.space.id, selected_date)
|
||||
|
||||
{:ok, bookings} =
|
||||
BookingSystem.list_active_asset_bookings_by_date(asset.id, selected_date)
|
||||
|
||||
if connected?(socket) do
|
||||
Phoenix.PubSub.subscribe(SpazioSolazzo.PubSub, "booking:created")
|
||||
Phoenix.PubSub.subscribe(SpazioSolazzo.PubSub, "booking:cancelled")
|
||||
end
|
||||
|
||||
{:ok,
|
||||
socket
|
||||
|> assign(
|
||||
asset: asset,
|
||||
space: asset.space,
|
||||
bookings: bookings,
|
||||
selected_date: selected_date,
|
||||
selected_time_slot: nil,
|
||||
show_booking_modal: false,
|
||||
show_success_modal: false,
|
||||
time_slots: time_slots
|
||||
)}
|
||||
|
||||
{:error, _error} ->
|
||||
{:ok,
|
||||
socket
|
||||
|> put_flash(:error, "Asset not found")
|
||||
|> push_navigate(to: "/")}
|
||||
end
|
||||
end
|
||||
|
||||
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)}
|
||||
end
|
||||
|
||||
def handle_event("cancel_booking", _params, socket) do
|
||||
{:noreply, assign(socket, show_booking_modal: false)}
|
||||
end
|
||||
|
||||
def handle_event("close_success_modal", _params, socket) do
|
||||
{:noreply, assign(socket, show_success_modal: false)}
|
||||
end
|
||||
|
||||
def handle_info({:create_booking, booking_data}, socket) do
|
||||
current_user = socket.assigns.current_user
|
||||
|
||||
result =
|
||||
BookingSystem.create_booking(
|
||||
socket.assigns.selected_time_slot.id,
|
||||
socket.assigns.asset.id,
|
||||
current_user.id,
|
||||
socket.assigns.selected_date,
|
||||
booking_data.customer_name,
|
||||
current_user.email,
|
||||
booking_data.customer_phone,
|
||||
booking_data.customer_comment
|
||||
)
|
||||
|
||||
case result do
|
||||
{:ok, _booking} ->
|
||||
{:noreply,
|
||||
socket
|
||||
|> assign(
|
||||
show_booking_modal: false,
|
||||
show_success_modal: true
|
||||
)}
|
||||
|
||||
{:error, _error} ->
|
||||
{:noreply,
|
||||
socket
|
||||
|> assign(show_booking_modal: false)
|
||||
|> put_flash(:error, "Failed to create booking.}")}
|
||||
end
|
||||
end
|
||||
|
||||
def handle_info(
|
||||
%{topic: "booking:created", payload: %{data: %{asset_id: asset_id, date: date}}},
|
||||
%{assigns: %{asset: %{id: asset_id}, selected_date: date}} = socket
|
||||
) do
|
||||
{:ok, bookings} = BookingSystem.list_active_asset_bookings_by_date(asset_id, date)
|
||||
{:noreply, assign(socket, bookings: bookings)}
|
||||
end
|
||||
|
||||
def handle_info(
|
||||
%{topic: "booking:cancelled", payload: %{data: %{asset_id: asset_id, date: date}}},
|
||||
%{assigns: %{asset: %{id: asset_id}, selected_date: date}} = socket
|
||||
) do
|
||||
{:ok, bookings} = BookingSystem.list_active_asset_bookings_by_date(asset_id, date)
|
||||
{:noreply, assign(socket, bookings: bookings)}
|
||||
end
|
||||
|
||||
def handle_info({:date_selected, date}, socket) do
|
||||
{:ok, time_slots} =
|
||||
BookingSystem.get_space_time_slots_by_date(socket.assigns.space.id, date)
|
||||
|
||||
{:ok, bookings} =
|
||||
BookingSystem.list_active_asset_bookings_by_date(socket.assigns.asset.id, date)
|
||||
|
||||
{:noreply,
|
||||
assign(socket,
|
||||
selected_date: date,
|
||||
time_slots: time_slots,
|
||||
bookings: bookings
|
||||
)}
|
||||
end
|
||||
|
||||
def handle_info(_msg, socket) do
|
||||
{:noreply, socket}
|
||||
end
|
||||
|
||||
defp slot_booked?(time_slot_id, bookings) do
|
||||
Enum.any?(bookings, fn booking ->
|
||||
booking.time_slot_template_id == time_slot_id
|
||||
end)
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -1,78 +0,0 @@
|
|||
<Layouts.app flash={@flash} current_user={@current_user}>
|
||||
<section class="mx-auto max-w-[1200px] px-6 py-10">
|
||||
<div class="mb-4">
|
||||
<.back_to_link
|
||||
navigate={"/#{@space.slug}"}
|
||||
value={"Back to #{@space.name}"}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="text-center mb-12">
|
||||
<h1 class="text-4xl md:text-5xl lg:text-6xl font-black text-base-content tracking-tight mb-4">
|
||||
{@asset.name}
|
||||
</h1>
|
||||
<p class="text-lg text-neutral max-w-2xl mx-auto">
|
||||
{@space.name} - Flexible booking options available
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="max-w-4xl mx-auto bg-base-100 rounded-3xl p-8 md:p-12 border border-base-200 shadow-xl">
|
||||
<h2 class="text-2xl font-bold text-base-content mb-8">
|
||||
Available Time Slots
|
||||
</h2>
|
||||
|
||||
<div class="mb-8 grid grid-cols-1 lg:grid-cols-2 gap-8">
|
||||
<.live_component
|
||||
module={SpazioSolazzoWeb.CalendarLiveComponent}
|
||||
id="booking-calendar"
|
||||
selected_date={@selected_date}
|
||||
/>
|
||||
|
||||
<div class="time-slots-wrapper">
|
||||
<p class="mb-4 text-neutral">
|
||||
Selected day:
|
||||
<span class="font-bold text-base-content">
|
||||
{SpazioSolazzo.CalendarExt.format_date(@selected_date)}
|
||||
</span>
|
||||
</p>
|
||||
|
||||
<div class="max-h-80 overflow-y-auto pr-4 space-y-3">
|
||||
<%= if @time_slots == [] do %>
|
||||
<div class="text-center py-8 text-neutral">
|
||||
No time slots available for this date
|
||||
</div>
|
||||
<% else %>
|
||||
<%= for time_slot <- @time_slots do %>
|
||||
<% booked = slot_booked?(time_slot.id, @bookings) %>
|
||||
<.time_slot booked={booked} time_slot={time_slot} />
|
||||
<% end %>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-8 pt-6 border-t border-base-200 text-center">
|
||||
<p class="text-base font-medium text-secondary flex items-center justify-center gap-2">
|
||||
<.icon name="hero-credit-card" class="w-5 h-5" /> Payment due upon arrival.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<.live_component
|
||||
module={SpazioSolazzoWeb.BookingFormLiveComponent}
|
||||
id="booking-modal"
|
||||
show={@show_booking_modal}
|
||||
selected_time_slot={@selected_time_slot}
|
||||
asset={@asset}
|
||||
selected_date={@selected_date}
|
||||
current_user={@current_user}
|
||||
on_cancel={JS.push("cancel_booking")}
|
||||
/>
|
||||
|
||||
<.booking_confirmation_modal
|
||||
id="success-modal"
|
||||
show={@show_success_modal}
|
||||
on_close={JS.push("close_success_modal")}
|
||||
/>
|
||||
</Layouts.app>
|
||||
|
|
@ -0,0 +1,61 @@
|
|||
defmodule SpazioSolazzoWeb.BookingCancellationLive do
|
||||
use SpazioSolazzoWeb, :live_view
|
||||
|
||||
alias SpazioSolazzo.BookingSystem
|
||||
alias SpazioSolazzo.BookingSystem.Booking.Token
|
||||
|
||||
def mount(%{"token" => token}, _session, socket) do
|
||||
case Token.verify(token) do
|
||||
{:ok, %{booking_id: booking_id, action: :cancel}} ->
|
||||
case Ash.get(SpazioSolazzo.BookingSystem.Booking, booking_id, load: [:space]) do
|
||||
{:ok, booking} ->
|
||||
if booking.state in [:requested, :accepted] do
|
||||
{:ok,
|
||||
assign(socket,
|
||||
booking: booking,
|
||||
token: token,
|
||||
cancellation_reason: "",
|
||||
show_success: false
|
||||
)}
|
||||
else
|
||||
{:ok,
|
||||
socket
|
||||
|> put_flash(:error, "This booking has already been cancelled or completed")
|
||||
|> push_navigate(to: "/")}
|
||||
end
|
||||
|
||||
{:error, _} ->
|
||||
{:ok,
|
||||
socket
|
||||
|> put_flash(:error, "Booking not found")
|
||||
|> push_navigate(to: "/")}
|
||||
end
|
||||
|
||||
{:error, _} ->
|
||||
{:ok,
|
||||
socket
|
||||
|> put_flash(:error, "Invalid or expired cancellation link")
|
||||
|> push_navigate(to: "/")}
|
||||
end
|
||||
end
|
||||
|
||||
def handle_event("validate", %{"reason" => reason}, socket) do
|
||||
{:noreply, assign(socket, cancellation_reason: reason)}
|
||||
end
|
||||
|
||||
def handle_event("cancel_booking", %{"reason" => reason}, socket) do
|
||||
if String.trim(reason) == "" do
|
||||
{:noreply, put_flash(socket, :error, "Please provide a reason for cancellation")}
|
||||
else
|
||||
booking = socket.assigns.booking
|
||||
|
||||
case BookingSystem.cancel_booking(booking, reason) do
|
||||
{:ok, _cancelled_booking} ->
|
||||
{:noreply, assign(socket, show_success: true)}
|
||||
|
||||
{:error, _error} ->
|
||||
{:noreply, put_flash(socket, :error, "Failed to cancel booking. Please try again.")}
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,99 @@
|
|||
<Layouts.app flash={@flash} current_user={@current_user}>
|
||||
<div class="min-h-screen bg-gradient-to-br from-slate-50 to-slate-100 py-12 px-4 sm:px-6 lg:px-8">
|
||||
<div class="max-w-2xl mx-auto">
|
||||
<%= if @show_success do %>
|
||||
<div class="bg-white rounded-2xl shadow-xl 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 mb-4">
|
||||
<svg
|
||||
class="h-10 w-10 text-green-600"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M5 13l4 4L19 7"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<h1 class="text-3xl font-bold text-slate-900 mb-2">Booking Cancelled</h1>
|
||||
<p class="text-slate-600">
|
||||
Your booking has been successfully cancelled. The administrator has been notified.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<.link
|
||||
navigate="/"
|
||||
class="inline-block bg-orange-500 text-white px-8 py-3 rounded-xl font-semibold hover:bg-orange-600 transition-colors"
|
||||
>
|
||||
Return to Home
|
||||
</.link>
|
||||
</div>
|
||||
<% else %>
|
||||
<div class="bg-white rounded-2xl shadow-xl p-8">
|
||||
<h1 class="text-3xl font-bold text-slate-900 mb-6">Cancel Booking</h1>
|
||||
|
||||
<div class="mb-8 p-6 bg-slate-50 rounded-xl">
|
||||
<h2 class="text-lg font-semibold text-slate-900 mb-4">Booking Details</h2>
|
||||
<div class="space-y-2 text-slate-700">
|
||||
<p>
|
||||
<strong>Space:</strong>
|
||||
{@booking.space.name}
|
||||
</p>
|
||||
<p>
|
||||
<strong>Date:</strong>
|
||||
{Calendar.strftime(@booking.date, "%A, %B %d, %Y")}
|
||||
</p>
|
||||
<p>
|
||||
<strong>Time:</strong>
|
||||
{@booking.start_time} - {@booking.end_time}
|
||||
</p>
|
||||
<p>
|
||||
<strong>Customer:</strong>
|
||||
{@booking.customer_name}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form phx-submit="cancel_booking" phx-change="validate">
|
||||
<div class="mb-6">
|
||||
<label for="reason" class="block text-sm font-semibold text-slate-900 mb-2">
|
||||
Cancellation Reason <span class="text-red-500">*</span>
|
||||
</label>
|
||||
<textarea
|
||||
id="reason"
|
||||
name="reason"
|
||||
rows="4"
|
||||
required
|
||||
value={@cancellation_reason}
|
||||
placeholder="Please let us know why you're cancelling..."
|
||||
class="w-full px-4 py-3 border border-slate-300 rounded-xl focus:outline-none focus:ring-2 focus:ring-orange-500 focus:border-transparent"
|
||||
></textarea>
|
||||
<p class="mt-2 text-sm text-slate-500">
|
||||
This helps us improve our service and understand your needs better.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col sm:flex-row gap-4">
|
||||
<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"
|
||||
>
|
||||
Confirm Cancellation
|
||||
</button>
|
||||
<.link
|
||||
navigate="/"
|
||||
class="flex-1 bg-slate-200 text-slate-700 px-6 py-3 rounded-xl font-semibold hover:bg-slate-300 transition-colors text-center"
|
||||
>
|
||||
Keep Booking
|
||||
</.link>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
</Layouts.app>
|
||||
|
|
@ -8,9 +8,12 @@ defmodule SpazioSolazzoWeb.BookingFormLiveComponent do
|
|||
alias SpazioSolazzo.CalendarExt
|
||||
|
||||
def update(assigns, socket) do
|
||||
current_user = assigns.current_user
|
||||
|
||||
initial_data = %{
|
||||
"customer_name" => assigns.current_user.name,
|
||||
"customer_phone" => assigns.current_user.phone_number || "",
|
||||
"customer_name" => (current_user && current_user.name) || "",
|
||||
"customer_email" => (current_user && current_user.email) || "",
|
||||
"customer_phone" => (current_user && current_user.phone_number) || "",
|
||||
"customer_comment" => ""
|
||||
}
|
||||
|
||||
|
|
@ -29,6 +32,7 @@ defmodule SpazioSolazzoWeb.BookingFormLiveComponent do
|
|||
def handle_event("submit_booking", params, socket) do
|
||||
booking_data = %{
|
||||
customer_name: params["customer_name"] || "",
|
||||
customer_email: params["customer_email"],
|
||||
customer_phone: params["customer_phone"] || "",
|
||||
customer_comment: params["customer_comment"] || ""
|
||||
}
|
||||
|
|
@ -44,13 +48,33 @@ defmodule SpazioSolazzoWeb.BookingFormLiveComponent do
|
|||
<:title>Complete Your Booking</:title>
|
||||
<:subtitle>
|
||||
<%= if @selected_time_slot do %>
|
||||
{@asset.name} | {CalendarExt.format_time_range(@selected_time_slot)} on {CalendarExt.format_date(
|
||||
{@space.name} | {CalendarExt.format_time_range(@selected_time_slot)} on {CalendarExt.format_date(
|
||||
@selected_date
|
||||
)}
|
||||
<% end %>
|
||||
</:subtitle>
|
||||
|
||||
<div>
|
||||
<%= if @slot_availability == :over_public_capacity do %>
|
||||
<div class="mb-6 p-4 bg-yellow-50 dark:bg-yellow-900/20 border-l-4 border-yellow-400 rounded">
|
||||
<div class="flex gap-3">
|
||||
<div class="flex-shrink-0">
|
||||
<.icon
|
||||
name="hero-exclamation-triangle"
|
||||
class="size-5 text-yellow-600 dark:text-yellow-400"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-sm font-semibold text-yellow-800 dark:text-yellow-300">
|
||||
High Demand Time Slot
|
||||
</p>
|
||||
<p class="text-sm text-yellow-700 dark:text-yellow-400 mt-1">
|
||||
This time slot is popular. Your request will be subject to admin approval based on availability.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
<.form
|
||||
for={@form}
|
||||
id="booking-form"
|
||||
|
|
@ -68,19 +92,31 @@ defmodule SpazioSolazzoWeb.BookingFormLiveComponent do
|
|||
placeholder="Your full name"
|
||||
/>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-base-content mb-2">
|
||||
Email
|
||||
</label>
|
||||
<div class="flex items-center gap-3 p-4 bg-secondary/5 rounded-xl border border-base-200">
|
||||
<div class="flex-shrink-0">
|
||||
<.icon name="hero-envelope" class="size-5 text-secondary" />
|
||||
<%= if @current_user do %>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-base-content mb-2">
|
||||
Email
|
||||
</label>
|
||||
<div class="flex items-center gap-3 p-4 bg-secondary/5 rounded-xl border border-base-200">
|
||||
<div class="flex-shrink-0">
|
||||
<.icon name="hero-envelope" class="size-5 text-secondary" />
|
||||
</div>
|
||||
<span class="text-sm font-medium text-base-content truncate">
|
||||
{@current_user.email}
|
||||
</span>
|
||||
</div>
|
||||
<span class="text-sm font-medium text-base-content truncate">
|
||||
{@current_user.email}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<% else %>
|
||||
<.input
|
||||
name="customer_email"
|
||||
id="customer_email"
|
||||
type="email"
|
||||
label="Email *"
|
||||
value={@form[:customer_email].value}
|
||||
required
|
||||
placeholder="your@email.com"
|
||||
/>
|
||||
<% end %>
|
||||
|
||||
<.input
|
||||
name="customer_phone"
|
||||
|
|
|
|||
151
lib/spazio_solazzo_web/live/booking/space_booking_live.ex
Normal file
151
lib/spazio_solazzo_web/live/booking/space_booking_live.ex
Normal file
|
|
@ -0,0 +1,151 @@
|
|||
defmodule SpazioSolazzoWeb.SpaceBookingLive do
|
||||
use SpazioSolazzoWeb, :live_view
|
||||
|
||||
alias SpazioSolazzo.BookingSystem
|
||||
require Ash.Query
|
||||
|
||||
def mount(%{"space_slug" => space_slug}, _session, socket) do
|
||||
case BookingSystem.get_space_by_slug(space_slug) do
|
||||
{:ok, space} ->
|
||||
selected_date = Date.utc_today()
|
||||
|
||||
{:ok, time_slots} =
|
||||
BookingSystem.get_space_time_slots_by_date(space.id, selected_date)
|
||||
|
||||
{:ok, bookings} =
|
||||
BookingSystem.list_accepted_space_bookings_by_date(space.id, selected_date)
|
||||
|
||||
if connected?(socket) do
|
||||
Phoenix.PubSub.subscribe(SpazioSolazzo.PubSub, "booking:created")
|
||||
Phoenix.PubSub.subscribe(SpazioSolazzo.PubSub, "booking:approved")
|
||||
Phoenix.PubSub.subscribe(SpazioSolazzo.PubSub, "booking:cancelled")
|
||||
end
|
||||
|
||||
{:ok,
|
||||
socket
|
||||
|> assign(
|
||||
space: space,
|
||||
bookings: bookings,
|
||||
selected_date: selected_date,
|
||||
selected_time_slot: nil,
|
||||
show_booking_modal: false,
|
||||
show_success_modal: false,
|
||||
time_slots: time_slots,
|
||||
current_scope: nil,
|
||||
slot_availability: %{}
|
||||
)
|
||||
|> compute_slot_availability()}
|
||||
|
||||
{:error, _error} ->
|
||||
{:ok,
|
||||
socket
|
||||
|> put_flash(:error, "Space not found")
|
||||
|> push_navigate(to: "/")}
|
||||
end
|
||||
end
|
||||
|
||||
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
|
||||
)}
|
||||
end
|
||||
|
||||
def handle_event("cancel_booking", _params, socket) do
|
||||
{:noreply, assign(socket, show_booking_modal: false)}
|
||||
end
|
||||
|
||||
def handle_event("close_success_modal", _params, socket) do
|
||||
{:noreply, assign(socket, show_success_modal: false)}
|
||||
end
|
||||
|
||||
def handle_info({:create_booking, booking_data}, socket) do
|
||||
current_user = socket.assigns.current_user
|
||||
|
||||
result =
|
||||
BookingSystem.request_booking(
|
||||
socket.assigns.space.id,
|
||||
current_user && current_user.id,
|
||||
socket.assigns.selected_date,
|
||||
socket.assigns.selected_time_slot.start_time,
|
||||
socket.assigns.selected_time_slot.end_time,
|
||||
%{
|
||||
name: booking_data.customer_name,
|
||||
email: (current_user && current_user.email) || booking_data.customer_email,
|
||||
phone: booking_data.customer_phone,
|
||||
comment: booking_data.customer_comment
|
||||
}
|
||||
)
|
||||
|
||||
case result do
|
||||
{:ok, _booking} ->
|
||||
{:noreply,
|
||||
socket
|
||||
|> assign(
|
||||
show_booking_modal: false,
|
||||
show_success_modal: true
|
||||
)}
|
||||
|
||||
{:error, _error} ->
|
||||
{:noreply,
|
||||
socket
|
||||
|> assign(show_booking_modal: false)
|
||||
|> put_flash(:error, "Failed to create booking request.")}
|
||||
end
|
||||
end
|
||||
|
||||
def handle_info({:date_selected, date}, socket) do
|
||||
{:ok, time_slots} =
|
||||
BookingSystem.get_space_time_slots_by_date(socket.assigns.space.id, date)
|
||||
|
||||
{:ok, bookings} =
|
||||
BookingSystem.list_accepted_space_bookings_by_date(socket.assigns.space.id, date)
|
||||
|
||||
{:noreply,
|
||||
socket
|
||||
|> assign(
|
||||
selected_date: date,
|
||||
time_slots: time_slots,
|
||||
bookings: bookings
|
||||
)
|
||||
|> compute_slot_availability()}
|
||||
end
|
||||
|
||||
def handle_info(
|
||||
%{topic: "booking:" <> _event, payload: %{data: %{space_id: space_id, date: date}}},
|
||||
%{assigns: %{space: %{id: space_id}, selected_date: date}} = socket
|
||||
) do
|
||||
{:ok, bookings} = BookingSystem.list_accepted_space_bookings_by_date(space_id, date)
|
||||
|
||||
{:noreply,
|
||||
socket
|
||||
|> assign(bookings: bookings)
|
||||
|> compute_slot_availability()}
|
||||
end
|
||||
|
||||
def handle_info(_msg, socket) do
|
||||
{:noreply, socket}
|
||||
end
|
||||
|
||||
defp compute_slot_availability(socket) do
|
||||
slot_availability =
|
||||
socket.assigns.time_slots
|
||||
|> Enum.map(fn time_slot ->
|
||||
{:ok, status} =
|
||||
BookingSystem.check_availability(
|
||||
socket.assigns.space.id,
|
||||
socket.assigns.selected_date,
|
||||
time_slot.start_time,
|
||||
time_slot.end_time
|
||||
)
|
||||
|
||||
{time_slot.id, status}
|
||||
end)
|
||||
|> Map.new()
|
||||
|
||||
assign(socket, slot_availability: slot_availability)
|
||||
end
|
||||
end
|
||||
166
lib/spazio_solazzo_web/live/booking/space_booking_live.html.heex
Normal file
166
lib/spazio_solazzo_web/live/booking/space_booking_live.html.heex
Normal file
|
|
@ -0,0 +1,166 @@
|
|||
<Layouts.app flash={@flash} current_user={@current_user}>
|
||||
<section class="mx-auto max-w-[1200px] px-6 py-10">
|
||||
<div class="mb-10">
|
||||
<.link
|
||||
navigate={"/#{@space.slug}"}
|
||||
class="inline-flex items-center gap-2 text-sm font-medium text-slate-500 hover:text-sky-500 dark:text-slate-400 dark:hover:text-white transition-colors"
|
||||
>
|
||||
<.icon name="hero-arrow-left" class="w-5 h-5" /> Back to {@space.name}
|
||||
</.link>
|
||||
</div>
|
||||
|
||||
<div class="text-center mb-12">
|
||||
<h1 class="text-4xl md:text-5xl lg:text-6xl font-black text-slate-900 dark:text-white tracking-tight mb-4">
|
||||
{@space.name}
|
||||
</h1>
|
||||
<p class="text-lg text-slate-600 dark:text-slate-400 max-w-2xl mx-auto">
|
||||
{@space.description} - Flexible booking options available
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="max-w-4xl mx-auto bg-white dark:bg-slate-800 rounded-3xl p-8 md:p-12 border border-slate-200 dark:border-slate-700 shadow-xl shadow-slate-200/50 dark:shadow-none">
|
||||
<h2 class="text-2xl font-bold text-slate-900 dark:text-white mb-8">
|
||||
Available Time Slots
|
||||
</h2>
|
||||
|
||||
<div class="mb-8 grid grid-cols-1 lg:grid-cols-2 gap-8">
|
||||
<.live_component
|
||||
module={SpazioSolazzoWeb.CalendarLiveComponent}
|
||||
id="booking-calendar"
|
||||
selected_date={@selected_date}
|
||||
/>
|
||||
|
||||
<div class="time-slots-wrapper">
|
||||
<p class="mb-4 text-slate-500 dark:text-slate-400">
|
||||
Selected day:
|
||||
<span class="font-bold text-slate-900 dark:text-white">
|
||||
{Calendar.strftime(@selected_date, "%A, %B %d, %Y")}
|
||||
</span>
|
||||
</p>
|
||||
|
||||
<div class="max-h-80 overflow-y-auto pr-4 space-y-3">
|
||||
<%= if @time_slots == [] do %>
|
||||
<div class="text-center py-8 text-slate-500 dark:text-slate-400">
|
||||
No time slots available for this date
|
||||
</div>
|
||||
<% else %>
|
||||
<%= for time_slot <- @time_slots do %>
|
||||
<% availability = Map.get(@slot_availability, time_slot.id, :available) %>
|
||||
<%= if availability != :over_real_capacity do %>
|
||||
<button
|
||||
phx-click="select_slot"
|
||||
phx-value-time_slot_id={time_slot.id}
|
||||
class={[
|
||||
"w-full p-4 rounded-xl border-2 transition-all duration-200 text-left",
|
||||
if(availability == :available,
|
||||
do:
|
||||
"border-green-200 bg-green-50 hover:border-green-500 hover:shadow-lg cursor-pointer dark:bg-green-900/20 dark:border-green-800 dark:hover:border-green-600",
|
||||
else:
|
||||
"border-yellow-200 bg-yellow-50 hover:border-yellow-500 hover:shadow-lg cursor-pointer dark:bg-yellow-900/20 dark:border-yellow-800 dark:hover:border-yellow-600"
|
||||
)
|
||||
]}
|
||||
>
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<div class="text-lg font-semibold text-slate-900 dark:text-white">
|
||||
{Calendar.strftime(time_slot.start_time, "%H:%M")} - {Calendar.strftime(
|
||||
time_slot.end_time,
|
||||
"%H:%M"
|
||||
)}
|
||||
</div>
|
||||
<%= if availability == :available do %>
|
||||
<div class="text-sm text-green-600 dark:text-green-400 font-medium mt-1">
|
||||
Available - Request Booking
|
||||
</div>
|
||||
<% else %>
|
||||
<div class="text-sm text-yellow-600 dark:text-yellow-400 font-medium mt-1">
|
||||
High Demand - Join Waitlist
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
<.icon
|
||||
name="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"
|
||||
)
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
</button>
|
||||
<% end %>
|
||||
<% end %>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-8 pt-6 border-t border-slate-200 dark:border-slate-700 text-center">
|
||||
<p class="text-base font-medium text-sky-500 dark:text-sky-400 flex items-center justify-center gap-2">
|
||||
<.icon name="hero-credit-card" class="w-5 h-5" /> Payment due upon arrival.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<.live_component
|
||||
module={SpazioSolazzoWeb.BookingFormLiveComponent}
|
||||
id="booking-modal"
|
||||
show={@show_booking_modal}
|
||||
selected_time_slot={@selected_time_slot}
|
||||
space={@space}
|
||||
selected_date={@selected_date}
|
||||
current_user={@current_user}
|
||||
slot_availability={
|
||||
if @selected_time_slot do
|
||||
Map.get(@slot_availability, @selected_time_slot.id, :available)
|
||||
else
|
||||
:available
|
||||
end
|
||||
}
|
||||
on_cancel={JS.push("cancel_booking")}
|
||||
/>
|
||||
|
||||
<%= 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"
|
||||
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">
|
||||
<svg
|
||||
class="h-10 w-10 text-green-600 dark:text-green-400"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M5 13l4 4L19 7"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<h3 class="text-2xl 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>
|
||||
</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"
|
||||
>
|
||||
Close
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
</Layouts.app>
|
||||
|
|
@ -2,24 +2,10 @@ defmodule SpazioSolazzoWeb.CoworkingLive do
|
|||
use SpazioSolazzoWeb, :live_view
|
||||
|
||||
alias SpazioSolazzo.BookingSystem
|
||||
import SpazioSolazzoWeb.LandingComponents
|
||||
|
||||
def mount(_params, _session, socket) do
|
||||
{:ok, space} = BookingSystem.get_space_by_slug("coworking")
|
||||
{:ok, assets} = BookingSystem.get_space_assets(space.id)
|
||||
|
||||
images = [
|
||||
"https://lh3.googleusercontent.com/aida-public/AB6AXuDmh_AkVuUoICqpHk1NdLuLdi0xQBOC8Hy9PrsSNz956igHFRhbNGsB8k0vSLe2U2NW1sxRVZm_dwR27Q4Db_f21XbYkLtfiRYob-j4ran1rTBB0bQAz4QLFSO1yL_cPhDIpAyvC069mDQ33-ckZgZ_yvFsIK_-_0Jj2NEOnDie684uaR7vKuiBWlsr-JmAsPzUp7Aik7Qbzozune348nBz1bvWkBNMCpMO3JV8hrYBo1i6JlUiGSuP3-5fWXKt8dKhxPUN-amjLFgh",
|
||||
"https://lh3.googleusercontent.com/aida-public/AB6AXuCh5O9cz1ruQFH0Pq3MzC_1HsWrLPHbWlfYEdB2dmPi0YDn2L23R5hseUZmb19XlEju1n4a24oD6pH5qiG4SvIemrD45PfKwvNlckpOG59IYz5WYrHzroq7L4Uq9Hxl0PTzU5m8R5k625w_MrdZKidyfM6OnzNJfM5J3XftFI5A9J7wD_BDHRKxq8gxAukUCesuYX8lGm3AhQAZQTjaUY5yeobjt-NCSrlfTzxmcUmibJSTnKZuwx-li4QtFr0wQrzHVLUZYiAhA251",
|
||||
"https://lh3.googleusercontent.com/aida-public/AB6AXuCanfiWzXqH3fBrE6U3phirIFZo5bgKG1aa8wnXCRC12yOXkcgnGUTRhxppIk61QUdQWF9KuFAtjhDEI9AACV-pM7yXyPKbOKognCARD-qbffFtCwGLidcLkoprLnNAW12C7TeRL6gOEBas3RI7jCf30JmzMmSqCjMx3lixgrOr6qlpbHZA4Eog_P41y5zXtn9Nqlq2eB6c7RYhiOIJzXVpMmfLR_qf0HTmOnx2poDbqKcLDcCM-p4S6aAwLxC-GYBmvEfWQ4meToCL"
|
||||
]
|
||||
|
||||
{:ok,
|
||||
socket
|
||||
|> assign(
|
||||
space: space,
|
||||
assets: assets,
|
||||
images: images
|
||||
)}
|
||||
{:ok, assign(socket, space: space)}
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -1,11 +1,15 @@
|
|||
<Layouts.app flash={@flash} current_user={@current_user}>
|
||||
<.page_header
|
||||
booking_path="#interactive-floor-plan"
|
||||
booking_label="Explore Desks & Book"
|
||||
booking_path={~p"/book/space/#{@space.slug}"}
|
||||
booking_label="Book Space"
|
||||
price="€25"
|
||||
price_unit="day"
|
||||
capacity="5 Desks"
|
||||
images={@images}
|
||||
capacity={@space.public_capacity}
|
||||
images={[
|
||||
"https://lh3.googleusercontent.com/aida-public/AB6AXuDmh_AkVuUoICqpHk1NdLuLdi0xQBOC8Hy9PrsSNz956igHFRhbNGsB8k0vSLe2U2NW1sxRVZm_dwR27Q4Db_f21XbYkLtfiRYob-j4ran1rTBB0bQAz4QLFSO1yL_cPhDIpAyvC069mDQ33-ckZgZ_yvFsIK_-_0Jj2NEOnDie684uaR7vKuiBWlsr-JmAsPzUp7Aik7Qbzozune348nBz1bvWkBNMCpMO3JV8hrYBo1i6JlUiGSuP3-5fWXKt8dKhxPUN-amjLFgh",
|
||||
"https://lh3.googleusercontent.com/aida-public/AB6AXuCh5O9cz1ruQFH0Pq3MzC_1HsWrLPHbWlfYEdB2dmPi0YDn2L23R5hseUZmb19XlEju1n4a24oD6pH5qiG4SvIemrD45PfKwvNlckpOG59IYz5WYrHzroq7L4Uq9Hxl0PTzU5m8R5k625w_MrdZKidyfM6OnzNJfM5J3XftFI5A9J7wD_BDHRKxq8gxAukUCesuYX8lGm3AhQAZQTjaUY5yeobjt-NCSrlfTzxmcUmibJSTnKZuwx-li4QtFr0wQrzHVLUZYiAhA251",
|
||||
"https://lh3.googleusercontent.com/aida-public/AB6AXuCanfiWzXqH3fBrE6U3phirIFZo5bgKG1aa8wnXCRC12yOXkcgnGUTRhxppIk61QUdQWF9KuFAtjhDEI9AACV-pM7yXyPKbOKognCARD-qbffFtCwGLidcLkoprLnNAW12C7TeRL6gOEBas3RI7jCf30JmzMmSqCjMx3lixgrOr6qlpbHZA4Eog_P41y5zXtn9Nqlq2eB6c7RYhiOIJzXVpMmfLR_qf0HTmOnx2poDbqKcLDcCM-p4S6aAwLxC-GYBmvEfWQ4meToCL"
|
||||
]}
|
||||
>
|
||||
<:title>{@space.name}</:title>
|
||||
<:description>
|
||||
|
|
@ -38,43 +42,6 @@
|
|||
/>
|
||||
</.features_section>
|
||||
|
||||
<section class="py-20 px-6 bg-base-100" id="interactive-floor-plan">
|
||||
<div class="mx-auto max-w-[1200px]">
|
||||
<div class="text-center mb-12">
|
||||
<h2 class="text-3xl font-bold text-base-content mb-4">
|
||||
Interactive Floor Plan
|
||||
</h2>
|
||||
<p class="text-neutral max-w-lg mx-auto">
|
||||
Select any desk to customize your booking details on the next page, where availability is confirmed.
|
||||
</p>
|
||||
</div>
|
||||
<div class="bg-base-200 rounded-3xl p-8 md:p-12 border border-base-300 shadow-xl relative overflow-hidden">
|
||||
<div class="absolute top-6 right-6 opacity-10 pointer-events-none select-none">
|
||||
<.icon name="hero-building-office-2" class="w-32 h-32 text-secondary" />
|
||||
</div>
|
||||
<div class="flex flex-col items-center gap-10">
|
||||
<div class="grid grid-cols-2 md:grid-cols-4 gap-6 w-full max-w-3xl">
|
||||
<.link
|
||||
:for={asset <- @assets}
|
||||
navigate={~p"/book/asset/#{asset.id}"}
|
||||
class="group relative flex flex-col items-center gap-3 cursor-pointer"
|
||||
>
|
||||
<div class="w-full aspect-[4/3] rounded-xl bg-base-100 border-2 border-base-200 group-hover:border-secondary group-hover:shadow-lg group-hover:shadow-secondary/20 transition-all duration-300 flex items-center justify-center relative">
|
||||
<.icon
|
||||
name="hero-computer-desktop"
|
||||
class="w-12 h-12 text-base-300 group-hover:text-secondary transition-colors"
|
||||
/>
|
||||
</div>
|
||||
<span class="text-sm font-bold text-neutral group-hover:text-secondary transition-colors">
|
||||
{asset.name}
|
||||
</span>
|
||||
</.link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<.house_rules title="Coworking House Rules">
|
||||
<:rule>Please keep phone calls to the dedicated booths.</:rule>
|
||||
<:rule>Clean your desk area before leaving.</:rule>
|
||||
|
|
|
|||
|
|
@ -2,24 +2,10 @@ defmodule SpazioSolazzoWeb.MeetingLive do
|
|||
use SpazioSolazzoWeb, :live_view
|
||||
|
||||
alias SpazioSolazzo.BookingSystem
|
||||
import SpazioSolazzoWeb.LandingComponents
|
||||
|
||||
def mount(_params, _session, socket) do
|
||||
{:ok, space} = BookingSystem.get_space_by_slug("meeting")
|
||||
{:ok, asset} = BookingSystem.get_asset_by_space_id(space.id)
|
||||
|
||||
images = [
|
||||
"https://lh3.googleusercontent.com/aida-public/AB6AXuDmh_AkVuUoICqpHk1NdLuLdi0xQBOC8Hy9PrsSNz956igHFRhbNGsB8k0vSLe2U2NW1sxRVZm_dwR27Q4Db_f21XbYkLtfiRYob-j4ran1rTBB0bQAz4QLFSO1yL_cPhDIpAyvC069mDQ33-ckZgZ_yvFsIK_-_0Jj2NEOnDie684uaR7vKuiBWlsr-JmAsPzUp7Aik7Qbzozune348nBz1bvWkBNMCpMO3JV8hrYBo1i6JlUiGSuP3-5fWXKt8dKhxPUN-amjLFgh",
|
||||
"https://lh3.googleusercontent.com/aida-public/AB6AXuCh5O9cz1ruQFH0Pq3MzC_1HsWrLPHbWlfYEdB2dmPi0YDn2L23R5hseUZmb19XlEju1n4a24oD6pH5qiG4SvIemrD45PfKwvNlckpOG59IYz5WYrHzroq7L4Uq9Hxl0PTzU5m8R5k625w_MrdZKidyfM6OnzNJfM5J3XftFI5A9J7wD_BDHRKxq8gxAukUCesuYX8lGm3AhQAZQTjaUY5yeobjt-NCSrlfTzxmcUmibJSTnKZuwx-li4QtFr0wQrzHVLUZYiAhA251",
|
||||
"https://lh3.googleusercontent.com/aida-public/AB6AXuCanfiWzXqH3fBrE6U3phirIFZo5bgKG1aa8wnXCRC12yOXkcgnGUTRhxppIk61QUdQWF9KuFAtjhDEI9AACV-pM7yXyPKbOKognCARD-qbffFtCwGLidcLkoprLnNAW12C7TeRL6gOEBas3RI7jCf30JmzMmSqCjMx3lixgrOr6qlpbHZA4Eog_P41y5zXtn9Nqlq2eB6c7RYhiOIJzXVpMmfLR_qf0HTmOnx2poDbqKcLDcCM-p4S6aAwLxC-GYBmvEfWQ4meToCL"
|
||||
]
|
||||
|
||||
{:ok,
|
||||
socket
|
||||
|> assign(
|
||||
space: space,
|
||||
asset: asset,
|
||||
images: images
|
||||
)}
|
||||
{:ok, assign(socket, space: space)}
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -1,62 +1,49 @@
|
|||
<Layouts.app flash={@flash} current_user={@current_user}>
|
||||
<.page_header
|
||||
booking_path={~p"/book/asset/#{@asset.id}"}
|
||||
price="€40"
|
||||
booking_path={~p"/book/space/#{@space.slug}"}
|
||||
booking_label="Book Space"
|
||||
price="€35"
|
||||
price_unit="hour"
|
||||
capacity="Up to 8 People"
|
||||
images={@images}
|
||||
capacity={@space.public_capacity}
|
||||
images={[
|
||||
"https://lh3.googleusercontent.com/aida-public/AB6AXuDmh_AkVuUoICqpHk1NdLuLdi0xQBOC8Hy9PrsSNz956igHFRhbNGsB8k0vSLe2U2NW1sxRVZm_dwR27Q4Db_f21XbYkLtfiRYob-j4ran1rTBB0bQAz4QLFSO1yL_cPhDIpAyvC069mDQ33-ckZgZ_yvFsIK_-_0Jj2NEOnDie684uaR7vKuiBWlsr-JmAsPzUp7Aik7Qbzozune348nBz1bvWkBNMCpMO3JV8hrYBo1i6JlUiGSuP3-5fWXKt8dKhxPUN-amjLFgh",
|
||||
"https://lh3.googleusercontent.com/aida-public/AB6AXuCh5O9cz1ruQFH0Pq3MzC_1HsWrLPHbWlfYEdB2dmPi0YDn2L23R5hseUZmb19XlEju1n4a24oD6pH5qiG4SvIemrD45PfKwvNlckpOG59IYz5WYrHzroq7L4Uq9Hxl0PTzU5m8R5k625w_MrdZKidyfM6OnzNJfM5J3XftFI5A9J7wD_BDHRKxq8gxAukUCesuYX8lGm3AhQAZQTjaUY5yeobjt-NCSrlfTzxmcUmibJSTnKZuwx-li4QtFr0wQrzHVLUZYiAhA251",
|
||||
"https://lh3.googleusercontent.com/aida-public/AB6AXuCanfiWzXqH3fBrE6U3phirIFZo5bgKG1aa8wnXCRC12yOXkcgnGUTRhxppIk61QUdQWF9KuFAtjhDEI9AACV-pM7yXyPKbOKognCARD-qbffFtCwGLidcLkoprLnNAW12C7TeRL6gOEBas3RI7jCf30JmzMmSqCjMx3lixgrOr6qlpbHZA4Eog_P41y5zXtn9Nqlq2eB6c7RYhiOIJzXVpMmfLR_qf0HTmOnx2poDbqKcLDcCM-p4S6aAwLxC-GYBmvEfWQ4meToCL"
|
||||
]}
|
||||
>
|
||||
<:title>{@space.name}</:title>
|
||||
<:description>
|
||||
A private, sun-drenched sanctuary designed for focus and collaboration. Step into a space where Sicilian charm meets modern productivity.
|
||||
A private, professional space equipped for your team meetings, client presentations, or brainstorming sessions.
|
||||
</:description>
|
||||
</.page_header>
|
||||
|
||||
<.features_section
|
||||
title="Everything you need to succeed"
|
||||
description="We've equipped the Meeting Room with top-tier amenities so you can focus on the agenda, not the logistics."
|
||||
title="Built for collaboration"
|
||||
description="A dedicated meeting environment with all the professional amenities you need."
|
||||
>
|
||||
<:feature
|
||||
icon="hero-tv"
|
||||
title="4K Presentation"
|
||||
description="Crystal clear 65" monitor ready for your slide decks. Connect via HDMI or wireless casting."
|
||||
color="sky"
|
||||
icon="hero-presentation-chart-line"
|
||||
title="Pro Presentation Setup"
|
||||
description="75-inch 4K display, wireless presentation tools, and high-quality audio system."
|
||||
color="blue"
|
||||
/>
|
||||
<:feature
|
||||
icon="hero-video-camera"
|
||||
title="Video Conferencing"
|
||||
description="Logitech Rally bar with AI framing and noise cancellation for seamless remote meetings."
|
||||
color="orange"
|
||||
/>
|
||||
<:feature
|
||||
icon="hero-pencil"
|
||||
title="Creative Tools"
|
||||
description="Wall-to-wall glass whiteboard, sticky notes, and markers to capture every brainstorming session."
|
||||
color="yellow"
|
||||
/>
|
||||
<:feature
|
||||
icon="hero-wifi"
|
||||
title="Fiber Internet"
|
||||
description="Dedicated 1Gbps symmetrical fiber line ensuring you never drop a call or buffer a video."
|
||||
description="Professional webcam and microphone setup optimized for hybrid meetings."
|
||||
color="emerald"
|
||||
/>
|
||||
<:feature
|
||||
icon="hero-home"
|
||||
title="Ergonomic Comfort"
|
||||
description="Herman Miller chairs and a solid oak table designed to keep you comfortable during long sessions."
|
||||
color="indigo"
|
||||
/>
|
||||
<:feature
|
||||
icon="hero-cake"
|
||||
title="Catering Available"
|
||||
description="Pre-order coffee carafes, Sicilian pastries, or light lunch options from local partners."
|
||||
title="Hospitality Station"
|
||||
description="Coffee, tea, and water service to keep your team refreshed and focused."
|
||||
color="purple"
|
||||
/>
|
||||
</.features_section>
|
||||
|
||||
<.house_rules title="House Rules">
|
||||
<:rule>Please clean the whiteboard after use.</:rule>
|
||||
<:rule>Outside food is allowed, but please be tidy.</:rule>
|
||||
<:rule>Cancel up to 24 hours before for a full refund.</:rule>
|
||||
<.house_rules title="Meeting Room Guidelines">
|
||||
<:rule>Please arrive on time and end on schedule.</:rule>
|
||||
<:rule>Reset the room to its original setup after use.</:rule>
|
||||
<:rule>Technical support is available upon request.</:rule>
|
||||
</.house_rules>
|
||||
</Layouts.app>
|
||||
|
|
|
|||
|
|
@ -3,24 +3,9 @@ defmodule SpazioSolazzoWeb.MusicLive do
|
|||
|
||||
alias SpazioSolazzo.BookingSystem
|
||||
|
||||
import SpazioSolazzoWeb.LandingComponents
|
||||
|
||||
def mount(_params, _session, socket) do
|
||||
{:ok, space} = BookingSystem.get_space_by_slug("music")
|
||||
{:ok, asset} = BookingSystem.get_asset_by_space_id(space.id)
|
||||
|
||||
images = [
|
||||
"https://lh3.googleusercontent.com/aida-public/AB6AXuD1wkxK48dk7i5XYX6JL-O1egrsdLjcmOg7N4EB76QtUvhzR7lZQadprIT9rLPsroUjazftFRpp_z26wb8lHaUW9XyucGlKG3qG40oT5iaKWwqVI1drNKJDJgVBkmNjw4u_D5vig_C1pf6bgGZnPaOV2tnnmlexxHJIDQYZzfg1GGwgywBvpGLz_u2_jvkbyMo3_m5roM09PjonFEfGIHxjm0vClW1DAOX45IrT87A85OdAXEu2EPyB8oW9WzmolOn4DFj22vKWSbVD",
|
||||
"https://lh3.googleusercontent.com/aida-public/AB6AXuB3fJu4mgZaw8GP1OC2SjquJZJmnRlY_OHD4fO4AAd_KHd5BYnW1i0egrskoEfK_uCdK4pQu5kMf8pF9h_KXE0wYQAROnTBTJ4YmBpHui9nv8wz44VENo2p-lA3rW8xhQhiYzlAhHJlhgOdZluVp9eYvsZxGM76QkDXMcBQz1Ka5ZfRMNgddo1RS76IPaxbQIvpOh_55uW87bAiGAvhcE8GrIi2ugpiJ64Rdou1uZLD1bPWxUvyLtpTFFLr2vfVjq7OpVYiGnLaGstS",
|
||||
"https://lh3.googleusercontent.com/aida-public/AB6AXuBVY6j_kvSUC0trHVnwvszpxZa58CpY0sGTF6m6lPQJkFlN-GnK1ofNaSn8PU1JnmPPAl7B196LoYq4SfawlDFrg2ADKKr65cOE0jq2L9w-cXrkPxE4poylrIeKX8zP1JsIS5obvU5_HAG074RjeYSWFsV5Z7wQF0ktZlYL6m462hsl-xdLQWQiBLZOHNsBf6jrZieUst9dKUlec6hzWOqcbIXuugBJW5fklJmMti9CDQynn1XZ5I5gZYEL47tW2Ku9u_zEEpfqmEKF"
|
||||
]
|
||||
|
||||
{:ok,
|
||||
socket
|
||||
|> assign(
|
||||
space: space,
|
||||
asset: asset,
|
||||
images: images
|
||||
)}
|
||||
{:ok, assign(socket, space: space)}
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -1,62 +1,49 @@
|
|||
<Layouts.app flash={@flash} current_user={@current_user}>
|
||||
<.page_header
|
||||
booking_path={~p"/book/asset/#{@asset.id}"}
|
||||
price="€25"
|
||||
booking_path={~p"/book/space/#{@space.slug}"}
|
||||
booking_label="Book Space"
|
||||
price="€50"
|
||||
price_unit="hour"
|
||||
capacity="Up to 5 People"
|
||||
images={@images}
|
||||
capacity={@space.public_capacity}
|
||||
images={[
|
||||
"https://lh3.googleusercontent.com/aida-public/AB6AXuD1wkxK48dk7i5XYX6JL-O1egrsdLjcmOg7N4EB76QtUvhzR7lZQadprIT9rLPsroUjazftFRpp_z26wb8lHaUW9XyucGlKG3qG40oT5iaKWwqVI1drNKJDJgVBkmNjw4u_D5vig_C1pf6bgGZnPaOV2tnnmlexxHJIDQYZzfg1GGwgywBvpGLz_u2_jvkbyMo3_m5roM09PjonFEfGIHxjm0vClW1DAOX45IrT87A85OdAXEu2EPyB8oW9WzmolOn4DFj22vKWSbVD",
|
||||
"https://lh3.googleusercontent.com/aida-public/AB6AXuB3fJu4mgZaw8GP1OC2SjquJZJmnRlY_OHD4fO4AAd_KHd5BYnW1i0egrskoEfK_uCdK4pQu5kMf8pF9h_KXE0wYQAROnTBTJ4YmBpHui9nv8wz44VENo2p-lA3rW8xhQhiYzlAhHJlhgOdZluVp9eYvsZxGM76QkDXMcBQz1Ka5ZfRMNgddo1RS76IPaxbQIvpOh_55uW87bAiGAvhcE8GrIi2ugpiJ64Rdou1uZLD1bPWxUvyLtpTFFLr2vfVjq7OpVYiGnLaGstS",
|
||||
"https://lh3.googleusercontent.com/aida-public/AB6AXuBVY6j_kvSUC0trHVnwvszpxZa58CpY0sGTF6m6lPQJkFlN-GnK1ofNaSn8PU1JnmPPAl7B196LoYq4SfawlDFrg2ADKKr65cOE0jq2L9w-cXrkPxE4poylrIeKX8zP1JsIS5obvU5_HAG074RjeYSWFsV5Z7wQF0ktZlYL6m462hsl-xdLQWQiBLZOHNsBf6jrZieUst9dKUlec6hzWOqcbIXuugBJW5fklJmMti9CDQynn1XZ5I5gZYEL47tW2Ku9u_zEEpfqmEKF"
|
||||
]}
|
||||
>
|
||||
<:title>{@space.name}</:title>
|
||||
<:description>
|
||||
A relaxed, creative space for jamming, practice sessions, or just unwinding with instruments. A casual vibe, not a soundproof studio, perfect for connecting through music.
|
||||
A professionally designed music rehearsal and recording space in the heart of Palermo. Perfect for bands, solo artists, and content creators.
|
||||
</:description>
|
||||
</.page_header>
|
||||
|
||||
<.features_section
|
||||
title="Jam, Practice, Create"
|
||||
description="The Music Room is equipped with essentials for a good session. It's not a pro studio, but it has soul and everything you need to start playing."
|
||||
title="Professional Audio Environment"
|
||||
description="State-of-the-art equipment and acoustically treated space for your creative sessions."
|
||||
>
|
||||
<:feature
|
||||
icon="hero-musical-note"
|
||||
title="House Instruments"
|
||||
description="Includes a digital piano, acoustic guitar, and a basic drum kit ready for your use."
|
||||
color="sky"
|
||||
/>
|
||||
<:feature
|
||||
icon="hero-speaker-wave"
|
||||
title="PA System"
|
||||
description="Two active speakers and a simple 4-channel mixer to plug in vocals or keyboards."
|
||||
color="orange"
|
||||
/>
|
||||
<:feature
|
||||
icon="hero-home"
|
||||
title="Relaxed Vibe"
|
||||
description="Comfortable seating, warm lighting, and rugs creates a cozy atmosphere for creative flow."
|
||||
color="yellow"
|
||||
title="Premium Sound System"
|
||||
description="High-end monitors, professional mixing console, and complete PA system ready to use."
|
||||
color="violet"
|
||||
/>
|
||||
<:feature
|
||||
icon="hero-microphone"
|
||||
title="Basic Mics"
|
||||
description="Two Shure SM58 microphones with stands available for vocals or acoustic instruments."
|
||||
color="emerald"
|
||||
/>
|
||||
<:feature
|
||||
icon="hero-speaker-x-mark"
|
||||
title="Not Soundproof"
|
||||
description="Please note this is a community space. Some sound bleed occurs; keep volumes reasonable."
|
||||
color="indigo"
|
||||
title="Recording Ready"
|
||||
description="Multi-track recording capability with professional condenser microphones and interfaces."
|
||||
color="pink"
|
||||
/>
|
||||
<:feature
|
||||
icon="hero-musical-note"
|
||||
title="Bring Your Gear"
|
||||
description="Feel free to bring your own amps, pedals, or specific instruments to dial in your tone."
|
||||
color="purple"
|
||||
title="Full Backline"
|
||||
description="Drum kit, bass and guitar amplifiers, keyboards - everything you need to play."
|
||||
color="orange"
|
||||
/>
|
||||
</.features_section>
|
||||
|
||||
<.house_rules title="Jam Session Rules">
|
||||
<:rule>Reset instruments to their original places.</:rule>
|
||||
<:rule>No drinks on top of the piano or amps.</:rule>
|
||||
<:rule>Be mindful of volume for our coworking neighbors.</:rule>
|
||||
<.house_rules title="Music Studio Guidelines">
|
||||
<:rule>Respect noise level limits and time slots.</:rule>
|
||||
<:rule>Handle equipment with care and return items to their places.</:rule>
|
||||
<:rule>Clean up after your session.</:rule>
|
||||
</.house_rules>
|
||||
</Layouts.app>
|
||||
|
|
|
|||
|
|
@ -26,7 +26,6 @@ defmodule SpazioSolazzoWeb.Router do
|
|||
pipe_through :browser
|
||||
|
||||
get "/bookings/confirm", BookingController, :confirm
|
||||
get "/bookings/cancel", BookingController, :cancel
|
||||
get "/sign-out", AuthController, :sign_out
|
||||
get "/auth/magic/sign-in", AuthController, :magic_sign_in
|
||||
get "/auth/failure", AuthController, :auth_failure
|
||||
|
|
@ -53,7 +52,8 @@ defmodule SpazioSolazzoWeb.Router do
|
|||
on_mount: [
|
||||
{SpazioSolazzoWeb.LiveUserAuth, :live_user_required}
|
||||
] do
|
||||
live "/book/asset/:asset_id", AssetBookingLive
|
||||
live "/book/space/:space_slug", SpaceBookingLive
|
||||
live "/bookings/cancel", BookingCancellationLive
|
||||
live "/profile", ProfileLive
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -1,29 +0,0 @@
|
|||
defmodule SpazioSolazzo.Repo.Migrations.SetPhoneNumberAsNullable do
|
||||
@moduledoc """
|
||||
Updates resources based on their most recent snapshots.
|
||||
|
||||
This file was autogenerated with `mix ash_postgres.generate_migrations`
|
||||
"""
|
||||
|
||||
use Ecto.Migration
|
||||
|
||||
def up do
|
||||
alter table(:users) do
|
||||
modify :phone_number, :text, null: true
|
||||
end
|
||||
|
||||
alter table(:bookings) do
|
||||
modify :customer_phone, :text, null: true
|
||||
end
|
||||
end
|
||||
|
||||
def down do
|
||||
alter table(:bookings) do
|
||||
modify :customer_phone, :text, null: false
|
||||
end
|
||||
|
||||
alter table(:users) do
|
||||
modify :phone_number, :text, null: false
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -1,21 +0,0 @@
|
|||
defmodule SpazioSolazzo.Repo.Migrations.AddRoleToUsers do
|
||||
@moduledoc """
|
||||
Updates resources based on their most recent snapshots.
|
||||
|
||||
This file was autogenerated with `mix ash_postgres.generate_migrations`
|
||||
"""
|
||||
|
||||
use Ecto.Migration
|
||||
|
||||
def up do
|
||||
alter table(:users) do
|
||||
add :role, :text, null: false, default: "customer"
|
||||
end
|
||||
end
|
||||
|
||||
def down do
|
||||
alter table(:users) do
|
||||
remove :role
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
defmodule SpazioSolazzo.Repo.Migrations.SetupResourcesExtensions1 do
|
||||
defmodule SpazioSolazzo.Repo.Migrations.CreateBaseResourcesExtensions1 do
|
||||
@moduledoc """
|
||||
Installs any extensions that are mentioned in the repo's `installed_extensions/0` callback
|
||||
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
defmodule SpazioSolazzo.Repo.Migrations.SetupResources do
|
||||
defmodule SpazioSolazzo.Repo.Migrations.CreateBaseResources do
|
||||
@moduledoc """
|
||||
Updates resources based on their most recent snapshots.
|
||||
|
||||
|
|
@ -12,7 +12,8 @@ defmodule SpazioSolazzo.Repo.Migrations.SetupResources do
|
|||
add :id, :uuid, null: false, default: fragment("gen_random_uuid()"), primary_key: true
|
||||
add :email, :citext, null: false
|
||||
add :name, :text, null: false
|
||||
add :phone_number, :text, null: false
|
||||
add :phone_number, :text
|
||||
add :role, :text, null: false, default: "customer"
|
||||
end
|
||||
|
||||
create unique_index(:users, [:email], name: "users_unique_email_index")
|
||||
|
|
@ -59,6 +60,8 @@ defmodule SpazioSolazzo.Repo.Migrations.SetupResources do
|
|||
add :name, :text, null: false
|
||||
add :description, :text, null: false
|
||||
add :slug, :text, null: false
|
||||
add :public_capacity, :bigint, null: false
|
||||
add :real_capacity, :bigint, null: false
|
||||
end
|
||||
|
||||
create unique_index(:spaces, [:name], name: "spaces_unique_name_index")
|
||||
|
|
@ -72,9 +75,11 @@ defmodule SpazioSolazzo.Repo.Migrations.SetupResources do
|
|||
add :customer_email, :text, null: false
|
||||
add :start_time, :time, null: false
|
||||
add :end_time, :time, null: false
|
||||
add :customer_phone, :text, null: false
|
||||
add :customer_phone, :text
|
||||
add :customer_comment, :text
|
||||
add :state, :text, null: false, default: "reserved"
|
||||
add :cancellation_reason, :text
|
||||
add :rejection_reason, :text
|
||||
add :state, :text, null: false, default: "requested"
|
||||
|
||||
add :inserted_at, :utc_datetime_usec,
|
||||
null: false,
|
||||
|
|
@ -84,87 +89,34 @@ defmodule SpazioSolazzo.Repo.Migrations.SetupResources do
|
|||
null: false,
|
||||
default: fragment("(now() AT TIME ZONE 'utc')")
|
||||
|
||||
add :asset_id, :uuid
|
||||
add :time_slot_template_id, :uuid
|
||||
add :user_id, :uuid
|
||||
end
|
||||
|
||||
create table(:assets, primary_key: false) do
|
||||
add :id, :uuid, null: false, default: fragment("gen_random_uuid()"), primary_key: true
|
||||
end
|
||||
|
||||
alter table(:bookings) do
|
||||
modify :asset_id,
|
||||
references(:assets,
|
||||
column: :id,
|
||||
name: "bookings_asset_id_fkey",
|
||||
type: :uuid,
|
||||
prefix: "public"
|
||||
)
|
||||
|
||||
modify :time_slot_template_id,
|
||||
references(:time_slot_templates,
|
||||
column: :id,
|
||||
name: "bookings_time_slot_template_id_fkey",
|
||||
type: :uuid,
|
||||
prefix: "public"
|
||||
)
|
||||
|
||||
modify :user_id,
|
||||
references(:users,
|
||||
column: :id,
|
||||
name: "bookings_user_id_fkey",
|
||||
type: :uuid,
|
||||
prefix: "public",
|
||||
on_delete: :nilify_all
|
||||
)
|
||||
end
|
||||
|
||||
create index(:bookings, [:user_id])
|
||||
|
||||
alter table(:assets) do
|
||||
add :name, :text, null: false
|
||||
|
||||
add :space_id,
|
||||
references(:spaces,
|
||||
column: :id,
|
||||
name: "assets_space_id_fkey",
|
||||
name: "bookings_space_id_fkey",
|
||||
type: :uuid,
|
||||
prefix: "public"
|
||||
), null: false
|
||||
|
||||
add :user_id,
|
||||
references(:users,
|
||||
column: :id,
|
||||
name: "bookings_user_id_fkey",
|
||||
type: :uuid,
|
||||
prefix: "public",
|
||||
on_delete: :nilify_all
|
||||
)
|
||||
end
|
||||
|
||||
create unique_index(:assets, [:name, :space_id], name: "assets_unique_name_per_space_index")
|
||||
create index(:bookings, [:user_id])
|
||||
end
|
||||
|
||||
def down do
|
||||
drop_if_exists unique_index(:assets, [:name, :space_id],
|
||||
name: "assets_unique_name_per_space_index"
|
||||
)
|
||||
|
||||
drop constraint(:assets, "assets_space_id_fkey")
|
||||
|
||||
alter table(:assets) do
|
||||
remove :space_id
|
||||
remove :name
|
||||
end
|
||||
|
||||
drop_if_exists index(:bookings, [:user_id])
|
||||
|
||||
drop constraint(:bookings, "bookings_asset_id_fkey")
|
||||
|
||||
drop constraint(:bookings, "bookings_time_slot_template_id_fkey")
|
||||
drop constraint(:bookings, "bookings_space_id_fkey")
|
||||
|
||||
drop constraint(:bookings, "bookings_user_id_fkey")
|
||||
|
||||
alter table(:bookings) do
|
||||
modify :user_id, :uuid
|
||||
modify :time_slot_template_id, :uuid
|
||||
modify :asset_id, :uuid
|
||||
end
|
||||
|
||||
drop table(:assets)
|
||||
|
||||
drop table(:bookings)
|
||||
|
||||
drop_if_exists unique_index(:spaces, [:slug], name: "spaces_unique_slug_index")
|
||||
|
|
@ -172,6 +124,8 @@ defmodule SpazioSolazzo.Repo.Migrations.SetupResources do
|
|||
drop_if_exists unique_index(:spaces, [:name], name: "spaces_unique_name_index")
|
||||
|
||||
alter table(:spaces) do
|
||||
remove :real_capacity
|
||||
remove :public_capacity
|
||||
remove :slug
|
||||
remove :description
|
||||
remove :name
|
||||
|
|
@ -17,45 +17,42 @@ case BookingSystem.Space |> Ash.read() do
|
|||
:ok
|
||||
end
|
||||
|
||||
# Create Coworking Space
|
||||
# Create Coworking Space (public_capacity: 10, real_capacity: 12)
|
||||
coworking =
|
||||
BookingSystem.create_space!("Arcipelago", "coworking", "Flexible desk spaces for remote work")
|
||||
BookingSystem.create_space!(
|
||||
"Arcipelago",
|
||||
"coworking",
|
||||
"Flexible desk spaces for remote work",
|
||||
10,
|
||||
12
|
||||
)
|
||||
|
||||
IO.puts("✓ Created Coworking space")
|
||||
|
||||
# Create Meeting Room Space
|
||||
# Create Meeting Room Space (public_capacity: 1, real_capacity: 1)
|
||||
meeting =
|
||||
BookingSystem.create_space!(
|
||||
"Media room",
|
||||
"meeting",
|
||||
"Private conference room for your meetings"
|
||||
"Private conference room for your meetings",
|
||||
1,
|
||||
1
|
||||
)
|
||||
|
||||
IO.puts("✓ Created Meeting Room space")
|
||||
|
||||
# Create Music Studio Space
|
||||
music = BookingSystem.create_space!("Hall", "music", "Tailored for band rehearsals.")
|
||||
# Create Music Studio Space (public_capacity: 1, real_capacity: 2)
|
||||
music =
|
||||
BookingSystem.create_space!(
|
||||
"Hall",
|
||||
"music",
|
||||
"Tailored for band rehearsals.",
|
||||
1,
|
||||
2
|
||||
)
|
||||
|
||||
IO.puts("✓ Created Music Studio space")
|
||||
|
||||
# Create Coworking Tables (Assets)
|
||||
tables =
|
||||
for i <- 1..5 do
|
||||
BookingSystem.create_asset!("Table #{i}", coworking.id)
|
||||
end
|
||||
|
||||
IO.puts("✓ Created #{length(tables)} coworking tables")
|
||||
|
||||
# Create Meeting Room Asset
|
||||
BookingSystem.create_asset!("Main Conference Room", meeting.id)
|
||||
|
||||
IO.puts("✓ Created meeting room asset")
|
||||
|
||||
# Create Music Studio Asset
|
||||
BookingSystem.create_asset!("Recording Studio", music.id)
|
||||
|
||||
IO.puts("✓ Created music studio asset")
|
||||
|
||||
# Create Coworking Time Slot Templates for each weekday
|
||||
coworking_slots = [
|
||||
%{start_time: ~T[09:00:00], end_time: ~T[13:00:00]},
|
||||
|
|
|
|||
|
|
@ -1,93 +0,0 @@
|
|||
{
|
||||
"attributes": [
|
||||
{
|
||||
"allow_nil?": false,
|
||||
"default": "fragment(\"gen_random_uuid()\")",
|
||||
"generated?": false,
|
||||
"precision": null,
|
||||
"primary_key?": true,
|
||||
"references": null,
|
||||
"scale": null,
|
||||
"size": null,
|
||||
"source": "id",
|
||||
"type": "uuid"
|
||||
},
|
||||
{
|
||||
"allow_nil?": false,
|
||||
"default": "nil",
|
||||
"generated?": false,
|
||||
"precision": null,
|
||||
"primary_key?": false,
|
||||
"references": null,
|
||||
"scale": null,
|
||||
"size": null,
|
||||
"source": "name",
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"allow_nil?": false,
|
||||
"default": "nil",
|
||||
"generated?": false,
|
||||
"precision": null,
|
||||
"primary_key?": false,
|
||||
"references": {
|
||||
"deferrable": false,
|
||||
"destination_attribute": "id",
|
||||
"destination_attribute_default": null,
|
||||
"destination_attribute_generated": null,
|
||||
"index?": false,
|
||||
"match_type": null,
|
||||
"match_with": null,
|
||||
"multitenancy": {
|
||||
"attribute": null,
|
||||
"global": null,
|
||||
"strategy": null
|
||||
},
|
||||
"name": "assets_space_id_fkey",
|
||||
"on_delete": null,
|
||||
"on_update": null,
|
||||
"primary_key?": true,
|
||||
"schema": "public",
|
||||
"table": "spaces"
|
||||
},
|
||||
"scale": null,
|
||||
"size": null,
|
||||
"source": "space_id",
|
||||
"type": "uuid"
|
||||
}
|
||||
],
|
||||
"base_filter": null,
|
||||
"check_constraints": [],
|
||||
"custom_indexes": [],
|
||||
"custom_statements": [],
|
||||
"has_create_action": true,
|
||||
"hash": "9FAD08E854AEBC8A2C7C7466BBC5254CAFB16C2A860034688C3C1142E183052C",
|
||||
"identities": [
|
||||
{
|
||||
"all_tenants?": false,
|
||||
"base_filter": null,
|
||||
"index_name": "assets_unique_name_per_space_index",
|
||||
"keys": [
|
||||
{
|
||||
"type": "atom",
|
||||
"value": "name"
|
||||
},
|
||||
{
|
||||
"type": "atom",
|
||||
"value": "space_id"
|
||||
}
|
||||
],
|
||||
"name": "unique_name_per_space",
|
||||
"nils_distinct?": true,
|
||||
"where": null
|
||||
}
|
||||
],
|
||||
"multitenancy": {
|
||||
"attribute": null,
|
||||
"global": null,
|
||||
"strategy": null
|
||||
},
|
||||
"repo": "Elixir.SpazioSolazzo.Repo",
|
||||
"schema": null,
|
||||
"table": "assets"
|
||||
}
|
||||
|
|
@ -1,244 +0,0 @@
|
|||
{
|
||||
"attributes": [
|
||||
{
|
||||
"allow_nil?": false,
|
||||
"default": "fragment(\"gen_random_uuid()\")",
|
||||
"generated?": false,
|
||||
"precision": null,
|
||||
"primary_key?": true,
|
||||
"references": null,
|
||||
"scale": null,
|
||||
"size": null,
|
||||
"source": "id",
|
||||
"type": "uuid"
|
||||
},
|
||||
{
|
||||
"allow_nil?": false,
|
||||
"default": "nil",
|
||||
"generated?": false,
|
||||
"precision": null,
|
||||
"primary_key?": false,
|
||||
"references": null,
|
||||
"scale": null,
|
||||
"size": null,
|
||||
"source": "date",
|
||||
"type": "date"
|
||||
},
|
||||
{
|
||||
"allow_nil?": false,
|
||||
"default": "nil",
|
||||
"generated?": false,
|
||||
"precision": null,
|
||||
"primary_key?": false,
|
||||
"references": null,
|
||||
"scale": null,
|
||||
"size": null,
|
||||
"source": "customer_name",
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"allow_nil?": false,
|
||||
"default": "nil",
|
||||
"generated?": false,
|
||||
"precision": null,
|
||||
"primary_key?": false,
|
||||
"references": null,
|
||||
"scale": null,
|
||||
"size": null,
|
||||
"source": "customer_email",
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"allow_nil?": false,
|
||||
"default": "nil",
|
||||
"generated?": false,
|
||||
"precision": null,
|
||||
"primary_key?": false,
|
||||
"references": null,
|
||||
"scale": null,
|
||||
"size": null,
|
||||
"source": "start_time",
|
||||
"type": "time"
|
||||
},
|
||||
{
|
||||
"allow_nil?": false,
|
||||
"default": "nil",
|
||||
"generated?": false,
|
||||
"precision": null,
|
||||
"primary_key?": false,
|
||||
"references": null,
|
||||
"scale": null,
|
||||
"size": null,
|
||||
"source": "end_time",
|
||||
"type": "time"
|
||||
},
|
||||
{
|
||||
"allow_nil?": true,
|
||||
"default": "nil",
|
||||
"generated?": false,
|
||||
"precision": null,
|
||||
"primary_key?": false,
|
||||
"references": null,
|
||||
"scale": null,
|
||||
"size": null,
|
||||
"source": "customer_phone",
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"allow_nil?": true,
|
||||
"default": "nil",
|
||||
"generated?": false,
|
||||
"precision": null,
|
||||
"primary_key?": false,
|
||||
"references": null,
|
||||
"scale": null,
|
||||
"size": null,
|
||||
"source": "customer_comment",
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"allow_nil?": false,
|
||||
"default": "\"reserved\"",
|
||||
"generated?": false,
|
||||
"precision": null,
|
||||
"primary_key?": false,
|
||||
"references": null,
|
||||
"scale": null,
|
||||
"size": null,
|
||||
"source": "state",
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"allow_nil?": false,
|
||||
"default": "fragment(\"(now() AT TIME ZONE 'utc')\")",
|
||||
"generated?": false,
|
||||
"precision": null,
|
||||
"primary_key?": false,
|
||||
"references": null,
|
||||
"scale": null,
|
||||
"size": null,
|
||||
"source": "inserted_at",
|
||||
"type": "utc_datetime_usec"
|
||||
},
|
||||
{
|
||||
"allow_nil?": false,
|
||||
"default": "fragment(\"(now() AT TIME ZONE 'utc')\")",
|
||||
"generated?": false,
|
||||
"precision": null,
|
||||
"primary_key?": false,
|
||||
"references": null,
|
||||
"scale": null,
|
||||
"size": null,
|
||||
"source": "updated_at",
|
||||
"type": "utc_datetime_usec"
|
||||
},
|
||||
{
|
||||
"allow_nil?": true,
|
||||
"default": "nil",
|
||||
"generated?": false,
|
||||
"precision": null,
|
||||
"primary_key?": false,
|
||||
"references": {
|
||||
"deferrable": false,
|
||||
"destination_attribute": "id",
|
||||
"destination_attribute_default": null,
|
||||
"destination_attribute_generated": null,
|
||||
"index?": false,
|
||||
"match_type": null,
|
||||
"match_with": null,
|
||||
"multitenancy": {
|
||||
"attribute": null,
|
||||
"global": null,
|
||||
"strategy": null
|
||||
},
|
||||
"name": "bookings_asset_id_fkey",
|
||||
"on_delete": null,
|
||||
"on_update": null,
|
||||
"primary_key?": true,
|
||||
"schema": "public",
|
||||
"table": "assets"
|
||||
},
|
||||
"scale": null,
|
||||
"size": null,
|
||||
"source": "asset_id",
|
||||
"type": "uuid"
|
||||
},
|
||||
{
|
||||
"allow_nil?": true,
|
||||
"default": "nil",
|
||||
"generated?": false,
|
||||
"precision": null,
|
||||
"primary_key?": false,
|
||||
"references": {
|
||||
"deferrable": false,
|
||||
"destination_attribute": "id",
|
||||
"destination_attribute_default": null,
|
||||
"destination_attribute_generated": null,
|
||||
"index?": false,
|
||||
"match_type": null,
|
||||
"match_with": null,
|
||||
"multitenancy": {
|
||||
"attribute": null,
|
||||
"global": null,
|
||||
"strategy": null
|
||||
},
|
||||
"name": "bookings_time_slot_template_id_fkey",
|
||||
"on_delete": null,
|
||||
"on_update": null,
|
||||
"primary_key?": true,
|
||||
"schema": "public",
|
||||
"table": "time_slot_templates"
|
||||
},
|
||||
"scale": null,
|
||||
"size": null,
|
||||
"source": "time_slot_template_id",
|
||||
"type": "uuid"
|
||||
},
|
||||
{
|
||||
"allow_nil?": true,
|
||||
"default": "nil",
|
||||
"generated?": false,
|
||||
"precision": null,
|
||||
"primary_key?": false,
|
||||
"references": {
|
||||
"deferrable": false,
|
||||
"destination_attribute": "id",
|
||||
"destination_attribute_default": null,
|
||||
"destination_attribute_generated": null,
|
||||
"index?": true,
|
||||
"match_type": null,
|
||||
"match_with": null,
|
||||
"multitenancy": {
|
||||
"attribute": null,
|
||||
"global": null,
|
||||
"strategy": null
|
||||
},
|
||||
"name": "bookings_user_id_fkey",
|
||||
"on_delete": "nilify",
|
||||
"on_update": null,
|
||||
"primary_key?": true,
|
||||
"schema": "public",
|
||||
"table": "users"
|
||||
},
|
||||
"scale": null,
|
||||
"size": null,
|
||||
"source": "user_id",
|
||||
"type": "uuid"
|
||||
}
|
||||
],
|
||||
"base_filter": null,
|
||||
"check_constraints": [],
|
||||
"custom_indexes": [],
|
||||
"custom_statements": [],
|
||||
"has_create_action": true,
|
||||
"hash": "1480C13D76AD8CE079362CC851CF250063914A40A6CA48182E3D3B5D83CD174A",
|
||||
"identities": [],
|
||||
"multitenancy": {
|
||||
"attribute": null,
|
||||
"global": null,
|
||||
"strategy": null
|
||||
},
|
||||
"repo": "Elixir.SpazioSolazzo.Repo",
|
||||
"schema": null,
|
||||
"table": "bookings"
|
||||
}
|
||||
|
|
@ -73,7 +73,7 @@
|
|||
"type": "time"
|
||||
},
|
||||
{
|
||||
"allow_nil?": false,
|
||||
"allow_nil?": true,
|
||||
"default": "nil",
|
||||
"generated?": false,
|
||||
"precision": null,
|
||||
|
|
@ -96,9 +96,33 @@
|
|||
"source": "customer_comment",
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"allow_nil?": true,
|
||||
"default": "nil",
|
||||
"generated?": false,
|
||||
"precision": null,
|
||||
"primary_key?": false,
|
||||
"references": null,
|
||||
"scale": null,
|
||||
"size": null,
|
||||
"source": "cancellation_reason",
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"allow_nil?": true,
|
||||
"default": "nil",
|
||||
"generated?": false,
|
||||
"precision": null,
|
||||
"primary_key?": false,
|
||||
"references": null,
|
||||
"scale": null,
|
||||
"size": null,
|
||||
"source": "rejection_reason",
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"allow_nil?": false,
|
||||
"default": "\"reserved\"",
|
||||
"default": "\"requested\"",
|
||||
"generated?": false,
|
||||
"precision": null,
|
||||
"primary_key?": false,
|
||||
|
|
@ -133,7 +157,7 @@
|
|||
"type": "utc_datetime_usec"
|
||||
},
|
||||
{
|
||||
"allow_nil?": true,
|
||||
"allow_nil?": false,
|
||||
"default": "nil",
|
||||
"generated?": false,
|
||||
"precision": null,
|
||||
|
|
@ -151,47 +175,16 @@
|
|||
"global": null,
|
||||
"strategy": null
|
||||
},
|
||||
"name": "bookings_asset_id_fkey",
|
||||
"name": "bookings_space_id_fkey",
|
||||
"on_delete": null,
|
||||
"on_update": null,
|
||||
"primary_key?": true,
|
||||
"schema": "public",
|
||||
"table": "assets"
|
||||
"table": "spaces"
|
||||
},
|
||||
"scale": null,
|
||||
"size": null,
|
||||
"source": "asset_id",
|
||||
"type": "uuid"
|
||||
},
|
||||
{
|
||||
"allow_nil?": true,
|
||||
"default": "nil",
|
||||
"generated?": false,
|
||||
"precision": null,
|
||||
"primary_key?": false,
|
||||
"references": {
|
||||
"deferrable": false,
|
||||
"destination_attribute": "id",
|
||||
"destination_attribute_default": null,
|
||||
"destination_attribute_generated": null,
|
||||
"index?": false,
|
||||
"match_type": null,
|
||||
"match_with": null,
|
||||
"multitenancy": {
|
||||
"attribute": null,
|
||||
"global": null,
|
||||
"strategy": null
|
||||
},
|
||||
"name": "bookings_time_slot_template_id_fkey",
|
||||
"on_delete": null,
|
||||
"on_update": null,
|
||||
"primary_key?": true,
|
||||
"schema": "public",
|
||||
"table": "time_slot_templates"
|
||||
},
|
||||
"scale": null,
|
||||
"size": null,
|
||||
"source": "time_slot_template_id",
|
||||
"source": "space_id",
|
||||
"type": "uuid"
|
||||
},
|
||||
{
|
||||
|
|
@ -231,7 +224,7 @@
|
|||
"custom_indexes": [],
|
||||
"custom_statements": [],
|
||||
"has_create_action": true,
|
||||
"hash": "D4E1D8A61AAAA83530EF07DE6BB15175AE052ADE62D06E538C222576218F0289",
|
||||
"hash": "0EFE49884DF5DC66BF3F1E125132A2F3E18DD66AC732121184A93707260C5225",
|
||||
"identities": [],
|
||||
"multitenancy": {
|
||||
"attribute": null,
|
||||
|
|
@ -47,6 +47,30 @@
|
|||
"size": null,
|
||||
"source": "slug",
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"allow_nil?": false,
|
||||
"default": "nil",
|
||||
"generated?": false,
|
||||
"precision": null,
|
||||
"primary_key?": false,
|
||||
"references": null,
|
||||
"scale": null,
|
||||
"size": null,
|
||||
"source": "public_capacity",
|
||||
"type": "bigint"
|
||||
},
|
||||
{
|
||||
"allow_nil?": false,
|
||||
"default": "nil",
|
||||
"generated?": false,
|
||||
"precision": null,
|
||||
"primary_key?": false,
|
||||
"references": null,
|
||||
"scale": null,
|
||||
"size": null,
|
||||
"source": "real_capacity",
|
||||
"type": "bigint"
|
||||
}
|
||||
],
|
||||
"base_filter": null,
|
||||
|
|
@ -54,7 +78,7 @@
|
|||
"custom_indexes": [],
|
||||
"custom_statements": [],
|
||||
"has_create_action": true,
|
||||
"hash": "29965B10015A4BDE39A648F85CE4FDB39DDFC21E0CB18903C7F9677E11B11D21",
|
||||
"hash": "8C04048A6F7E0263FAA5078E0561DF61F4F7858EACAEB62A63F41D3A11DB3317",
|
||||
"identities": [
|
||||
{
|
||||
"all_tenants?": false,
|
||||
|
|
@ -85,7 +85,7 @@
|
|||
"custom_indexes": [],
|
||||
"custom_statements": [],
|
||||
"has_create_action": true,
|
||||
"hash": "F8562173C786461A330298CE68DE2EC79F3FD8380966F2F9E318E6010DEE466E",
|
||||
"hash": "E90087CB07EEC08732A24BCFC5114E66CCF992BDAE093E32565BDB922781F3FF",
|
||||
"identities": [],
|
||||
"multitenancy": {
|
||||
"attribute": null,
|
||||
|
|
@ -90,7 +90,7 @@
|
|||
"custom_indexes": [],
|
||||
"custom_statements": [],
|
||||
"has_create_action": true,
|
||||
"hash": "BEBC6DCB3A49C5C7A830CD684C51A1F901F7AB857C72B559F98A6D9ABAB6DB95",
|
||||
"hash": "A243D17701FAFD73D4B00DECBADC36EDBD6E58479228892FFEC235CA2A1119A4",
|
||||
"identities": [],
|
||||
"multitenancy": {
|
||||
"attribute": null,
|
||||
|
|
@ -1,82 +0,0 @@
|
|||
{
|
||||
"attributes": [
|
||||
{
|
||||
"allow_nil?": false,
|
||||
"default": "fragment(\"gen_random_uuid()\")",
|
||||
"generated?": false,
|
||||
"precision": null,
|
||||
"primary_key?": true,
|
||||
"references": null,
|
||||
"scale": null,
|
||||
"size": null,
|
||||
"source": "id",
|
||||
"type": "uuid"
|
||||
},
|
||||
{
|
||||
"allow_nil?": false,
|
||||
"default": "nil",
|
||||
"generated?": false,
|
||||
"precision": null,
|
||||
"primary_key?": false,
|
||||
"references": null,
|
||||
"scale": null,
|
||||
"size": null,
|
||||
"source": "email",
|
||||
"type": "citext"
|
||||
},
|
||||
{
|
||||
"allow_nil?": false,
|
||||
"default": "nil",
|
||||
"generated?": false,
|
||||
"precision": null,
|
||||
"primary_key?": false,
|
||||
"references": null,
|
||||
"scale": null,
|
||||
"size": null,
|
||||
"source": "name",
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"allow_nil?": false,
|
||||
"default": "nil",
|
||||
"generated?": false,
|
||||
"precision": null,
|
||||
"primary_key?": false,
|
||||
"references": null,
|
||||
"scale": null,
|
||||
"size": null,
|
||||
"source": "phone_number",
|
||||
"type": "text"
|
||||
}
|
||||
],
|
||||
"base_filter": null,
|
||||
"check_constraints": [],
|
||||
"custom_indexes": [],
|
||||
"custom_statements": [],
|
||||
"has_create_action": true,
|
||||
"hash": "57A4F6BB98AF78EE399E42DAC6AAD95429A43D3A110B792DD6A05735591A5C62",
|
||||
"identities": [
|
||||
{
|
||||
"all_tenants?": false,
|
||||
"base_filter": null,
|
||||
"index_name": "users_unique_email_index",
|
||||
"keys": [
|
||||
{
|
||||
"type": "atom",
|
||||
"value": "email"
|
||||
}
|
||||
],
|
||||
"name": "unique_email",
|
||||
"nils_distinct?": true,
|
||||
"where": null
|
||||
}
|
||||
],
|
||||
"multitenancy": {
|
||||
"attribute": null,
|
||||
"global": null,
|
||||
"strategy": null
|
||||
},
|
||||
"repo": "Elixir.SpazioSolazzo.Repo",
|
||||
"schema": null,
|
||||
"table": "users"
|
||||
}
|
||||
|
|
@ -1,82 +0,0 @@
|
|||
{
|
||||
"attributes": [
|
||||
{
|
||||
"allow_nil?": false,
|
||||
"default": "fragment(\"gen_random_uuid()\")",
|
||||
"generated?": false,
|
||||
"precision": null,
|
||||
"primary_key?": true,
|
||||
"references": null,
|
||||
"scale": null,
|
||||
"size": null,
|
||||
"source": "id",
|
||||
"type": "uuid"
|
||||
},
|
||||
{
|
||||
"allow_nil?": false,
|
||||
"default": "nil",
|
||||
"generated?": false,
|
||||
"precision": null,
|
||||
"primary_key?": false,
|
||||
"references": null,
|
||||
"scale": null,
|
||||
"size": null,
|
||||
"source": "email",
|
||||
"type": "citext"
|
||||
},
|
||||
{
|
||||
"allow_nil?": false,
|
||||
"default": "nil",
|
||||
"generated?": false,
|
||||
"precision": null,
|
||||
"primary_key?": false,
|
||||
"references": null,
|
||||
"scale": null,
|
||||
"size": null,
|
||||
"source": "name",
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"allow_nil?": true,
|
||||
"default": "nil",
|
||||
"generated?": false,
|
||||
"precision": null,
|
||||
"primary_key?": false,
|
||||
"references": null,
|
||||
"scale": null,
|
||||
"size": null,
|
||||
"source": "phone_number",
|
||||
"type": "text"
|
||||
}
|
||||
],
|
||||
"base_filter": null,
|
||||
"check_constraints": [],
|
||||
"custom_indexes": [],
|
||||
"custom_statements": [],
|
||||
"has_create_action": true,
|
||||
"hash": "6D20D84C076B09FEAFF7A68C69930AF48936A628BBEE432695F0ECC02B0F4EFA",
|
||||
"identities": [
|
||||
{
|
||||
"all_tenants?": false,
|
||||
"base_filter": null,
|
||||
"index_name": "users_unique_email_index",
|
||||
"keys": [
|
||||
{
|
||||
"type": "atom",
|
||||
"value": "email"
|
||||
}
|
||||
],
|
||||
"name": "unique_email",
|
||||
"nils_distinct?": true,
|
||||
"where": null
|
||||
}
|
||||
],
|
||||
"multitenancy": {
|
||||
"attribute": null,
|
||||
"global": null,
|
||||
"strategy": null
|
||||
},
|
||||
"repo": "Elixir.SpazioSolazzo.Repo",
|
||||
"schema": null,
|
||||
"table": "users"
|
||||
}
|
||||
|
|
@ -66,7 +66,7 @@
|
|||
"custom_indexes": [],
|
||||
"custom_statements": [],
|
||||
"has_create_action": true,
|
||||
"hash": "0B279BA9251EA91451BB80DF86AF81ECCA5395011864BE01BE1C369420AD9C18",
|
||||
"hash": "C0CABA03FB9BA869F1460B25693E0222AB1CE8B59A15BB0E343DAB3F4F3949E4",
|
||||
"identities": [
|
||||
{
|
||||
"all_tenants?": false,
|
||||
|
|
@ -43,14 +43,15 @@ defmodule SpazioSolazzo.Accounts.UserTest do
|
|||
describe "terminate_account with delete_history: false (anonymization)" do
|
||||
test "deletes user but preserves bookings with nullified user_id" do
|
||||
user = register_user("delete@example.com")
|
||||
{_space, asset, time_slot} = create_booking_fixtures()
|
||||
{space, _time_slot} = create_booking_fixtures()
|
||||
|
||||
{:ok, booking1} =
|
||||
BookingSystem.create_booking(
|
||||
time_slot.id,
|
||||
asset.id,
|
||||
space.id,
|
||||
user.id,
|
||||
Date.utc_today(),
|
||||
~T[09:00:00],
|
||||
~T[11:00:00],
|
||||
"John Doe",
|
||||
"john@example.com",
|
||||
"+393627384027",
|
||||
|
|
@ -59,10 +60,11 @@ defmodule SpazioSolazzo.Accounts.UserTest do
|
|||
|
||||
{:ok, booking2} =
|
||||
BookingSystem.create_booking(
|
||||
time_slot.id,
|
||||
asset.id,
|
||||
space.id,
|
||||
user.id,
|
||||
Date.add(Date.utc_today(), 1),
|
||||
~T[09:00:00],
|
||||
~T[11:00:00],
|
||||
"Jane Doe",
|
||||
"jane@example.com",
|
||||
"+393627384028",
|
||||
|
|
@ -88,60 +90,46 @@ defmodule SpazioSolazzo.Accounts.UserTest do
|
|||
|
||||
test "cancels future confirmed bookings before anonymizing" do
|
||||
user = register_user("cancel@example.com")
|
||||
{_space, asset, time_slot} = create_booking_fixtures()
|
||||
{space, _time_slot} = create_booking_fixtures()
|
||||
|
||||
future_date = Date.add(Date.utc_today(), 7)
|
||||
|
||||
{:ok, future_booking} =
|
||||
BookingSystem.create_booking(
|
||||
time_slot.id,
|
||||
asset.id,
|
||||
space.id,
|
||||
user.id,
|
||||
future_date,
|
||||
~T[09:00:00],
|
||||
~T[11:00:00],
|
||||
"Future User",
|
||||
"future@example.com",
|
||||
"+393627384029",
|
||||
"future booking"
|
||||
)
|
||||
|
||||
{:ok, past_booking} =
|
||||
BookingSystem.create_booking(
|
||||
time_slot.id,
|
||||
asset.id,
|
||||
user.id,
|
||||
Date.add(Date.utc_today(), -7),
|
||||
"Past User",
|
||||
"past@example.com",
|
||||
"+393627384030",
|
||||
"past booking"
|
||||
)
|
||||
|
||||
future_booking_id = future_booking.id
|
||||
past_booking_id = past_booking.id
|
||||
|
||||
:ok = Accounts.terminate_account(user, false, actor: user)
|
||||
|
||||
{:ok, cancelled_booking} = Ash.get(Booking, future_booking_id, authorize?: false)
|
||||
{:ok, preserved_past_booking} = Ash.get(Booking, past_booking_id, authorize?: false)
|
||||
|
||||
assert cancelled_booking.state == :cancelled
|
||||
assert cancelled_booking.user_id == nil
|
||||
|
||||
assert preserved_past_booking.user_id == nil
|
||||
end
|
||||
end
|
||||
|
||||
describe "terminate_account with delete_history: true (hard delete)" do
|
||||
test "deletes user and all associated bookings permanently" do
|
||||
user = register_user("harddelete@example.com")
|
||||
{_space, asset, time_slot} = create_booking_fixtures()
|
||||
{space, _time_slot} = create_booking_fixtures()
|
||||
|
||||
{:ok, booking1} =
|
||||
BookingSystem.create_booking(
|
||||
time_slot.id,
|
||||
asset.id,
|
||||
space.id,
|
||||
user.id,
|
||||
Date.utc_today(),
|
||||
~T[09:00:00],
|
||||
~T[11:00:00],
|
||||
"Delete Me",
|
||||
"deleteme@example.com",
|
||||
"+393627384031",
|
||||
|
|
@ -150,10 +138,11 @@ defmodule SpazioSolazzo.Accounts.UserTest do
|
|||
|
||||
{:ok, booking2} =
|
||||
BookingSystem.create_booking(
|
||||
time_slot.id,
|
||||
asset.id,
|
||||
space.id,
|
||||
user.id,
|
||||
Date.add(Date.utc_today(), 1),
|
||||
~T[09:00:00],
|
||||
~T[11:00:00],
|
||||
"Delete Me Too",
|
||||
"deletemetoo@example.com",
|
||||
"+393627384032",
|
||||
|
|
@ -189,11 +178,11 @@ defmodule SpazioSolazzo.Accounts.UserTest do
|
|||
BookingSystem.create_space(
|
||||
"Test Space #{unique_id}",
|
||||
"test-space-#{unique_id}",
|
||||
"Test description"
|
||||
"Test description",
|
||||
10,
|
||||
12
|
||||
)
|
||||
|
||||
{:ok, asset} = BookingSystem.create_asset("Test Asset", space.id)
|
||||
|
||||
{:ok, time_slot} =
|
||||
BookingSystem.create_time_slot_template(
|
||||
~T[09:00:00],
|
||||
|
|
@ -202,6 +191,6 @@ defmodule SpazioSolazzo.Accounts.UserTest do
|
|||
space.id
|
||||
)
|
||||
|
||||
{space, asset, time_slot}
|
||||
{space, time_slot}
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -1,51 +0,0 @@
|
|||
defmodule SpazioSolazzo.BookingSystem.AssetTest do
|
||||
use ExUnit.Case, async: true
|
||||
use SpazioSolazzo.DataCase
|
||||
|
||||
alias SpazioSolazzo.BookingSystem
|
||||
alias SpazioSolazzo.BookingSystem.Asset
|
||||
|
||||
setup do
|
||||
{:ok, space} = BookingSystem.create_space("AssetSpace", "assetspace", "desc")
|
||||
%{space: space}
|
||||
end
|
||||
|
||||
test "prevents duplicate asset names within the system", %{space: space} do
|
||||
assert {:ok, _} = BookingSystem.create_asset("T1", space.id)
|
||||
assert {:error, error} = BookingSystem.create_asset("T1", space.id)
|
||||
|
||||
message = Ash.Error.error_descriptions(error)
|
||||
|
||||
assert String.contains?(message, "already been taken")
|
||||
end
|
||||
|
||||
test "allows same asset name for different spaces", %{space: space} do
|
||||
assert {:ok, _} = BookingSystem.create_asset("T1", space.id)
|
||||
|
||||
# create another space
|
||||
{:ok, other_space} = BookingSystem.create_space("OtherSpace", "otherspace", "desc")
|
||||
|
||||
# same name in different space should succeed
|
||||
assert {:ok, _} = BookingSystem.create_asset("T1", other_space.id)
|
||||
end
|
||||
|
||||
test "can get single asset by space id", %{space: space} do
|
||||
assert {:ok, expected_asset} = BookingSystem.create_asset("T1", space.id)
|
||||
assert {:ok, asset} = BookingSystem.get_asset_by_space_id(space.id)
|
||||
assert asset.id == expected_asset.id
|
||||
end
|
||||
|
||||
test "can get multiple assets by space id", %{space: space} do
|
||||
assert {:ok, _} = BookingSystem.create_asset("T1", space.id)
|
||||
assert {:ok, _} = BookingSystem.create_asset("T2", space.id)
|
||||
assert {:ok, _} = BookingSystem.create_asset("T3", space.id)
|
||||
|
||||
assert {:ok,
|
||||
[
|
||||
%Asset{name: "T1"},
|
||||
%Asset{name: "T2"},
|
||||
%Asset{name: "T3"}
|
||||
]} =
|
||||
BookingSystem.get_space_assets(space.id)
|
||||
end
|
||||
end
|
||||
|
|
@ -2,233 +2,735 @@ defmodule SpazioSolazzo.BookingSystem.BookingTest do
|
|||
use ExUnit.Case, async: true
|
||||
use SpazioSolazzo.DataCase
|
||||
|
||||
import SpazioSolazzo.AuthHelpers
|
||||
|
||||
alias SpazioSolazzo.BookingSystem
|
||||
alias SpazioSolazzo.BookingSystem.Booking
|
||||
alias SpazioSolazzo.BookingSystem.Booking.EmailWorker
|
||||
|
||||
setup do
|
||||
{:ok, space} = BookingSystem.create_space("Test", "test2", "desc")
|
||||
{:ok, asset} = BookingSystem.create_asset("Table 1", space.id)
|
||||
|
||||
{:ok, time_slot} =
|
||||
BookingSystem.create_time_slot_template(
|
||||
~T[09:00:00],
|
||||
~T[18:00:00],
|
||||
:monday,
|
||||
space.id
|
||||
{:ok, space} =
|
||||
BookingSystem.create_space(
|
||||
"Test Space",
|
||||
"test-space",
|
||||
"Test description",
|
||||
2,
|
||||
3
|
||||
)
|
||||
|
||||
user = register_user("test@example.com")
|
||||
user = register_user("testuser@example.com", "Test User")
|
||||
|
||||
%{space: space, asset: asset, time_slot: time_slot, user: user}
|
||||
date = ~D[2026-02-10]
|
||||
|
||||
%{space: space, user: user, date: date}
|
||||
end
|
||||
|
||||
test "it can create a booking from a time slot template", %{
|
||||
asset: asset,
|
||||
time_slot: time_slot,
|
||||
user: user
|
||||
} do
|
||||
{:ok, booking} =
|
||||
BookingSystem.create_booking(
|
||||
time_slot.id,
|
||||
asset.id,
|
||||
user.id,
|
||||
Date.utc_today(),
|
||||
"John",
|
||||
"john@example.com",
|
||||
"+393627384027",
|
||||
"test"
|
||||
)
|
||||
describe "request_booking/5" do
|
||||
test "creates a booking request successfully", %{space: space, date: date} do
|
||||
assert {:ok, booking} =
|
||||
BookingSystem.request_booking(
|
||||
space.id,
|
||||
nil,
|
||||
date,
|
||||
~T[09:00:00],
|
||||
~T[10:00:00],
|
||||
%{
|
||||
name: "John Doe",
|
||||
email: "john@example.com",
|
||||
phone: "+39 1234567890",
|
||||
comment: "Test booking"
|
||||
}
|
||||
)
|
||||
|
||||
assert booking.start_time == time_slot.start_time
|
||||
assert booking.end_time == time_slot.end_time
|
||||
assert booking.state == :reserved
|
||||
assert booking.user_id == user.id
|
||||
assert booking.space_id == space.id
|
||||
assert booking.user_id == nil
|
||||
assert booking.date == date
|
||||
assert booking.start_time == ~T[09:00:00]
|
||||
assert booking.end_time == ~T[10:00:00]
|
||||
assert booking.customer_name == "John Doe"
|
||||
assert booking.customer_email == "john@example.com"
|
||||
assert booking.customer_phone == "+39 1234567890"
|
||||
assert booking.customer_comment == "Test booking"
|
||||
assert booking.state == :requested
|
||||
end
|
||||
|
||||
test "creates booking with authenticated user", %{space: space, user: user, date: date} do
|
||||
assert {:ok, booking} =
|
||||
BookingSystem.request_booking(
|
||||
space.id,
|
||||
user.id,
|
||||
date,
|
||||
~T[09:00:00],
|
||||
~T[10:00:00],
|
||||
%{
|
||||
name: "John Doe",
|
||||
email: user.email,
|
||||
phone: "+39 1234567890",
|
||||
comment: ""
|
||||
}
|
||||
)
|
||||
|
||||
assert booking.user_id == user.id
|
||||
assert to_string(booking.customer_email) == to_string(user.email)
|
||||
end
|
||||
|
||||
test "rejects booking with end time before start time", %{space: space, date: date} do
|
||||
assert {:error, error} =
|
||||
BookingSystem.request_booking(
|
||||
space.id,
|
||||
nil,
|
||||
date,
|
||||
~T[10:00:00],
|
||||
~T[09:00:00],
|
||||
%{
|
||||
name: "John Doe",
|
||||
email: "john@example.com",
|
||||
phone: "",
|
||||
comment: ""
|
||||
}
|
||||
)
|
||||
|
||||
error_messages = Ash.Error.error_descriptions(error)
|
||||
assert String.contains?(error_messages, "must be after start time")
|
||||
end
|
||||
|
||||
test "rejects booking in the past", %{space: space} do
|
||||
past_date = Date.add(Date.utc_today(), -1)
|
||||
|
||||
assert {:error, error} =
|
||||
BookingSystem.request_booking(
|
||||
space.id,
|
||||
nil,
|
||||
past_date,
|
||||
~T[09:00:00],
|
||||
~T[10:00:00],
|
||||
%{
|
||||
name: "John Doe",
|
||||
email: "john@example.com",
|
||||
phone: "",
|
||||
comment: ""
|
||||
}
|
||||
)
|
||||
|
||||
error_messages = Ash.Error.error_descriptions(error)
|
||||
assert String.contains?(error_messages, "cannot be in the past")
|
||||
end
|
||||
|
||||
test "allows booking for today", %{space: space} do
|
||||
today = Date.utc_today()
|
||||
|
||||
assert {:ok, booking} =
|
||||
BookingSystem.request_booking(
|
||||
space.id,
|
||||
nil,
|
||||
today,
|
||||
~T[09:00:00],
|
||||
~T[10:00:00],
|
||||
%{
|
||||
name: "John Doe",
|
||||
email: "john@example.com",
|
||||
phone: "",
|
||||
comment: ""
|
||||
}
|
||||
)
|
||||
|
||||
assert booking.date == today
|
||||
end
|
||||
|
||||
test "requires customer name and email", %{space: space, date: date} do
|
||||
assert {:error, _error} =
|
||||
BookingSystem.request_booking(
|
||||
space.id,
|
||||
nil,
|
||||
date,
|
||||
~T[09:00:00],
|
||||
~T[10:00:00],
|
||||
%{
|
||||
name: "",
|
||||
email: "",
|
||||
phone: "",
|
||||
comment: ""
|
||||
}
|
||||
)
|
||||
end
|
||||
|
||||
test "phone number is optional", %{space: space, date: date} do
|
||||
assert {:ok, booking} =
|
||||
BookingSystem.request_booking(
|
||||
space.id,
|
||||
nil,
|
||||
date,
|
||||
~T[09:00:00],
|
||||
~T[10:00:00],
|
||||
%{
|
||||
name: "John Doe",
|
||||
email: "john@example.com",
|
||||
phone: "",
|
||||
comment: ""
|
||||
}
|
||||
)
|
||||
|
||||
assert booking.customer_phone == nil || booking.customer_phone == ""
|
||||
end
|
||||
end
|
||||
|
||||
test "it sends a confirmation email after the booking is created", %{
|
||||
asset: asset,
|
||||
time_slot: time_slot,
|
||||
user: user
|
||||
} do
|
||||
{:ok, booking} =
|
||||
BookingSystem.create_booking(
|
||||
time_slot.id,
|
||||
asset.id,
|
||||
user.id,
|
||||
Date.utc_today(),
|
||||
"John",
|
||||
"john@example.com",
|
||||
"+393627384027",
|
||||
"test"
|
||||
)
|
||||
describe "approve_booking/1" do
|
||||
test "approves a pending booking", %{space: space, date: date} do
|
||||
{:ok, booking} =
|
||||
BookingSystem.request_booking(
|
||||
space.id,
|
||||
nil,
|
||||
date,
|
||||
~T[09:00:00],
|
||||
~T[10:00:00],
|
||||
%{
|
||||
name: "John Doe",
|
||||
email: "john@example.com",
|
||||
phone: "",
|
||||
comment: ""
|
||||
}
|
||||
)
|
||||
|
||||
formatted_date = Calendar.strftime(booking.date, "%A, %B %d")
|
||||
assert booking.state == :requested
|
||||
|
||||
assert_enqueued worker: EmailWorker,
|
||||
args: %{
|
||||
"booking_id" => booking.id,
|
||||
"customer_name" => booking.customer_name,
|
||||
"customer_email" => booking.customer_email,
|
||||
"date" => formatted_date,
|
||||
"start_time" => booking.start_time,
|
||||
"end_time" => booking.end_time
|
||||
}
|
||||
{:ok, approved_booking} = BookingSystem.approve_booking(booking.id)
|
||||
|
||||
assert approved_booking.state == :accepted
|
||||
assert approved_booking.id == booking.id
|
||||
end
|
||||
|
||||
test "cannot approve already approved booking", %{space: space, date: date} do
|
||||
{:ok, booking} =
|
||||
BookingSystem.request_booking(
|
||||
space.id,
|
||||
nil,
|
||||
date,
|
||||
~T[09:00:00],
|
||||
~T[10:00:00],
|
||||
%{
|
||||
name: "John Doe",
|
||||
email: "john@example.com",
|
||||
phone: "",
|
||||
comment: ""
|
||||
}
|
||||
)
|
||||
|
||||
{:ok, _} = BookingSystem.approve_booking(booking.id)
|
||||
|
||||
assert {:error, error} = BookingSystem.approve_booking(booking.id)
|
||||
error_messages = Ash.Error.error_descriptions(error)
|
||||
assert String.contains?(error_messages, "no matching transition")
|
||||
end
|
||||
|
||||
test "cannot approve cancelled booking", %{space: space, date: date} do
|
||||
{:ok, booking} =
|
||||
BookingSystem.request_booking(
|
||||
space.id,
|
||||
nil,
|
||||
date,
|
||||
~T[09:00:00],
|
||||
~T[10:00:00],
|
||||
%{
|
||||
name: "John Doe",
|
||||
email: "john@example.com",
|
||||
phone: "",
|
||||
comment: ""
|
||||
}
|
||||
)
|
||||
|
||||
{:ok, _} = BookingSystem.cancel_booking(booking.id, "Test cancellation")
|
||||
|
||||
assert {:error, error} = BookingSystem.approve_booking(booking.id)
|
||||
error_messages = Ash.Error.error_descriptions(error)
|
||||
assert String.contains?(error_messages, "no matching transition")
|
||||
end
|
||||
end
|
||||
|
||||
test "it can confirm a booking was paid", %{asset: asset, time_slot: time_slot, user: user} do
|
||||
{:ok, booking} =
|
||||
BookingSystem.create_booking(
|
||||
time_slot.id,
|
||||
asset.id,
|
||||
user.id,
|
||||
Date.utc_today(),
|
||||
"John",
|
||||
"john@example.com",
|
||||
"+393627384027",
|
||||
"test"
|
||||
)
|
||||
describe "cancel_booking/1" do
|
||||
test "cancels a pending booking", %{space: space, date: date} do
|
||||
{:ok, booking} =
|
||||
BookingSystem.request_booking(
|
||||
space.id,
|
||||
nil,
|
||||
date,
|
||||
~T[09:00:00],
|
||||
~T[10:00:00],
|
||||
%{
|
||||
name: "John Doe",
|
||||
email: "john@example.com",
|
||||
phone: "",
|
||||
comment: ""
|
||||
}
|
||||
)
|
||||
|
||||
assert booking.state == :reserved
|
||||
{:ok, cancelled_booking} = BookingSystem.cancel_booking(booking.id, "Test cancellation")
|
||||
|
||||
assert {:ok, booking} = BookingSystem.confirm_booking(booking)
|
||||
assert cancelled_booking.state == :cancelled
|
||||
assert cancelled_booking.id == booking.id
|
||||
end
|
||||
|
||||
assert booking.state == :completed
|
||||
test "cancels an approved booking", %{space: space, date: date} do
|
||||
{:ok, booking} =
|
||||
BookingSystem.request_booking(
|
||||
space.id,
|
||||
nil,
|
||||
date,
|
||||
~T[09:00:00],
|
||||
~T[10:00:00],
|
||||
%{
|
||||
name: "John Doe",
|
||||
email: "john@example.com",
|
||||
phone: "",
|
||||
comment: ""
|
||||
}
|
||||
)
|
||||
|
||||
{:ok, _} = BookingSystem.approve_booking(booking.id)
|
||||
{:ok, cancelled_booking} = BookingSystem.cancel_booking(booking.id, "Test cancellation")
|
||||
|
||||
assert cancelled_booking.state == :cancelled
|
||||
end
|
||||
|
||||
test "cannot cancel already cancelled booking", %{space: space, date: date} do
|
||||
{:ok, booking} =
|
||||
BookingSystem.request_booking(
|
||||
space.id,
|
||||
nil,
|
||||
date,
|
||||
~T[09:00:00],
|
||||
~T[10:00:00],
|
||||
%{
|
||||
name: "John Doe",
|
||||
email: "john@example.com",
|
||||
phone: "",
|
||||
comment: ""
|
||||
}
|
||||
)
|
||||
|
||||
{:ok, _} = BookingSystem.cancel_booking(booking.id, "Test cancellation")
|
||||
|
||||
assert {:error, error} = BookingSystem.cancel_booking(booking.id, "Test cancellation")
|
||||
error_messages = Ash.Error.error_descriptions(error)
|
||||
assert String.contains?(error_messages, "no matching transition")
|
||||
end
|
||||
end
|
||||
|
||||
test "it can cancel a booking", %{asset: asset, time_slot: time_slot, user: user} do
|
||||
{:ok, booking} =
|
||||
BookingSystem.create_booking(
|
||||
time_slot.id,
|
||||
asset.id,
|
||||
user.id,
|
||||
Date.utc_today(),
|
||||
"John",
|
||||
"john@example.com",
|
||||
"+393627384027",
|
||||
"test"
|
||||
)
|
||||
describe "list_accepted_space_bookings_by_date/2" do
|
||||
test "returns only approved bookings for specific date", %{space: space, date: date} do
|
||||
{:ok, approved1} =
|
||||
BookingSystem.request_booking(
|
||||
space.id,
|
||||
nil,
|
||||
date,
|
||||
~T[09:00:00],
|
||||
~T[10:00:00],
|
||||
%{name: "User 1", email: "user1@example.com", phone: "", comment: ""}
|
||||
)
|
||||
|
||||
assert booking.state == :reserved
|
||||
{:ok, _} = BookingSystem.approve_booking(approved1.id)
|
||||
|
||||
assert {:ok, booking} = BookingSystem.cancel_booking(booking)
|
||||
{:ok, approved2} =
|
||||
BookingSystem.request_booking(
|
||||
space.id,
|
||||
nil,
|
||||
date,
|
||||
~T[10:00:00],
|
||||
~T[11:00:00],
|
||||
%{name: "User 2", email: "user2@example.com", phone: "", comment: ""}
|
||||
)
|
||||
|
||||
assert booking.state == :cancelled
|
||||
{:ok, _} = BookingSystem.approve_booking(approved2.id)
|
||||
|
||||
{:ok, _pending} =
|
||||
BookingSystem.request_booking(
|
||||
space.id,
|
||||
nil,
|
||||
date,
|
||||
~T[11:00:00],
|
||||
~T[12:00:00],
|
||||
%{name: "User 3", email: "user3@example.com", phone: "", comment: ""}
|
||||
)
|
||||
|
||||
{:ok, bookings} = BookingSystem.list_accepted_space_bookings_by_date(space.id, date)
|
||||
|
||||
assert length(bookings) == 2
|
||||
assert Enum.all?(bookings, &(&1.state == :accepted))
|
||||
end
|
||||
|
||||
test "does not return cancelled bookings", %{space: space, date: date} do
|
||||
{:ok, booking} =
|
||||
BookingSystem.request_booking(
|
||||
space.id,
|
||||
nil,
|
||||
date,
|
||||
~T[09:00:00],
|
||||
~T[10:00:00],
|
||||
%{name: "User 1", email: "user1@example.com", phone: "", comment: ""}
|
||||
)
|
||||
|
||||
{: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)
|
||||
|
||||
assert bookings == []
|
||||
end
|
||||
|
||||
test "only returns bookings for specified date", %{space: space, date: date} do
|
||||
other_date = Date.add(date, 1)
|
||||
|
||||
{:ok, booking1} =
|
||||
BookingSystem.request_booking(
|
||||
space.id,
|
||||
nil,
|
||||
date,
|
||||
~T[09:00:00],
|
||||
~T[10:00:00],
|
||||
%{name: "User 1", email: "user1@example.com", phone: "", comment: ""}
|
||||
)
|
||||
|
||||
{:ok, _} = BookingSystem.approve_booking(booking1.id)
|
||||
|
||||
{:ok, booking2} =
|
||||
BookingSystem.request_booking(
|
||||
space.id,
|
||||
nil,
|
||||
other_date,
|
||||
~T[09:00:00],
|
||||
~T[10:00:00],
|
||||
%{name: "User 2", email: "user2@example.com", phone: "", comment: ""}
|
||||
)
|
||||
|
||||
{:ok, _} = BookingSystem.approve_booking(booking2.id)
|
||||
|
||||
{:ok, bookings} = BookingSystem.list_accepted_space_bookings_by_date(space.id, date)
|
||||
|
||||
assert length(bookings) == 1
|
||||
assert hd(bookings).date == date
|
||||
end
|
||||
|
||||
test "only returns bookings for specified space", %{space: space, date: date} do
|
||||
{:ok, other_space} =
|
||||
BookingSystem.create_space(
|
||||
"Other Space",
|
||||
"other-space",
|
||||
"Other description",
|
||||
5,
|
||||
5
|
||||
)
|
||||
|
||||
{:ok, booking} =
|
||||
BookingSystem.request_booking(
|
||||
other_space.id,
|
||||
nil,
|
||||
date,
|
||||
~T[09:00:00],
|
||||
~T[10:00:00],
|
||||
%{name: "User 1", email: "user1@example.com", phone: "", comment: ""}
|
||||
)
|
||||
|
||||
{:ok, _} = BookingSystem.approve_booking(booking.id)
|
||||
|
||||
{:ok, bookings} = BookingSystem.list_accepted_space_bookings_by_date(space.id, date)
|
||||
|
||||
assert bookings == []
|
||||
end
|
||||
end
|
||||
|
||||
test "it can list asset bookings by date", %{
|
||||
asset: asset,
|
||||
space: space,
|
||||
time_slot: time_slot,
|
||||
user: user
|
||||
} do
|
||||
{:ok, asset2} = BookingSystem.create_asset("Table 2", space.id)
|
||||
{:ok, asset3} = BookingSystem.create_asset("Table 3", space.id)
|
||||
today_date = Date.utc_today()
|
||||
describe "list_booking_requests/3" do
|
||||
test "returns pending and approved bookings for space", %{space: space, date: date} do
|
||||
{:ok, pending} =
|
||||
BookingSystem.request_booking(
|
||||
space.id,
|
||||
nil,
|
||||
date,
|
||||
~T[09:00:00],
|
||||
~T[10:00:00],
|
||||
%{name: "User 1", email: "user1@example.com", phone: "", comment: ""}
|
||||
)
|
||||
|
||||
{:ok, time_slot2} =
|
||||
BookingSystem.create_time_slot_template(~T[13:00:00], ~T[18:00:00], :tuesday, space.id)
|
||||
{:ok, approved} =
|
||||
BookingSystem.request_booking(
|
||||
space.id,
|
||||
nil,
|
||||
date,
|
||||
~T[10:00:00],
|
||||
~T[11:00:00],
|
||||
%{name: "User 2", email: "user2@example.com", phone: "", comment: ""}
|
||||
)
|
||||
|
||||
{:ok, time_slot3} =
|
||||
BookingSystem.create_time_slot_template(~T[09:00:00], ~T[13:00:00], :tuesday, space.id)
|
||||
{:ok, _} = BookingSystem.approve_booking(approved.id)
|
||||
|
||||
# Create the bookings we want to query
|
||||
assert {:ok, _} =
|
||||
BookingSystem.create_booking(
|
||||
time_slot2.id,
|
||||
asset.id,
|
||||
user.id,
|
||||
today_date,
|
||||
"John",
|
||||
"john@example.com",
|
||||
"+393627384027",
|
||||
"test"
|
||||
)
|
||||
{:ok, cancelled} =
|
||||
BookingSystem.request_booking(
|
||||
space.id,
|
||||
nil,
|
||||
date,
|
||||
~T[11:00:00],
|
||||
~T[12:00:00],
|
||||
%{name: "User 3", email: "user3@example.com", phone: "", comment: ""}
|
||||
)
|
||||
|
||||
assert {:ok, _} =
|
||||
BookingSystem.create_booking(
|
||||
time_slot3.id,
|
||||
asset.id,
|
||||
user.id,
|
||||
today_date,
|
||||
"John",
|
||||
"john@example.com",
|
||||
"+393627384027",
|
||||
"test"
|
||||
)
|
||||
{:ok, _} = BookingSystem.cancel_booking(cancelled.id, "Test cancellation")
|
||||
|
||||
# Create bookings for asset but another date
|
||||
assert {:ok, _} =
|
||||
BookingSystem.create_booking(
|
||||
time_slot2.id,
|
||||
asset.id,
|
||||
user.id,
|
||||
Date.add(today_date, 1),
|
||||
"John",
|
||||
"john@example.com",
|
||||
"+393627384027",
|
||||
"test"
|
||||
)
|
||||
{:ok, bookings} = BookingSystem.list_booking_requests(space.id, nil, nil)
|
||||
|
||||
# Create bookings for other assets
|
||||
assert {:ok, _} =
|
||||
BookingSystem.create_booking(
|
||||
time_slot.id,
|
||||
asset2.id,
|
||||
user.id,
|
||||
today_date,
|
||||
"John",
|
||||
"john@example.com",
|
||||
"+393627384027",
|
||||
"test"
|
||||
)
|
||||
assert length(bookings) == 2
|
||||
assert Enum.any?(bookings, &(&1.id == pending.id))
|
||||
assert Enum.any?(bookings, &(&1.id == approved.id))
|
||||
refute Enum.any?(bookings, &(&1.id == cancelled.id))
|
||||
end
|
||||
|
||||
assert {:ok, _} =
|
||||
BookingSystem.create_booking(
|
||||
time_slot.id,
|
||||
asset3.id,
|
||||
user.id,
|
||||
today_date,
|
||||
"John",
|
||||
"john@example.com",
|
||||
"+393627384027",
|
||||
"test"
|
||||
)
|
||||
test "filters by email", %{space: space, date: date} do
|
||||
{:ok, _booking1} =
|
||||
BookingSystem.request_booking(
|
||||
space.id,
|
||||
nil,
|
||||
date,
|
||||
~T[09:00:00],
|
||||
~T[10:00:00],
|
||||
%{name: "User 1", email: "user1@example.com", phone: "", comment: ""}
|
||||
)
|
||||
|
||||
assert {:ok, bookings} =
|
||||
BookingSystem.list_active_asset_bookings_by_date(asset.id, Date.utc_today())
|
||||
{:ok, booking2} =
|
||||
BookingSystem.request_booking(
|
||||
space.id,
|
||||
nil,
|
||||
date,
|
||||
~T[10:00:00],
|
||||
~T[11:00:00],
|
||||
%{name: "User 2", email: "user2@example.com", phone: "", comment: ""}
|
||||
)
|
||||
|
||||
asset_id = asset.id
|
||||
{:ok, bookings} =
|
||||
BookingSystem.list_booking_requests(space.id, "user2@example.com", nil)
|
||||
|
||||
assert [
|
||||
%Booking{date: ^today_date, asset_id: ^asset_id},
|
||||
%Booking{date: ^today_date, asset_id: ^asset_id}
|
||||
] = bookings
|
||||
assert length(bookings) == 1
|
||||
assert hd(bookings).id == booking2.id
|
||||
end
|
||||
|
||||
test "filters by date", %{space: space, date: date} do
|
||||
other_date = Date.add(date, 1)
|
||||
|
||||
{:ok, booking1} =
|
||||
BookingSystem.request_booking(
|
||||
space.id,
|
||||
nil,
|
||||
date,
|
||||
~T[09:00:00],
|
||||
~T[10:00:00],
|
||||
%{name: "User 1", email: "user1@example.com", phone: "", comment: ""}
|
||||
)
|
||||
|
||||
{:ok, _booking2} =
|
||||
BookingSystem.request_booking(
|
||||
space.id,
|
||||
nil,
|
||||
other_date,
|
||||
~T[09:00:00],
|
||||
~T[10:00:00],
|
||||
%{name: "User 2", email: "user2@example.com", phone: "", comment: ""}
|
||||
)
|
||||
|
||||
{:ok, bookings} = BookingSystem.list_booking_requests(space.id, nil, date)
|
||||
|
||||
assert length(bookings) == 1
|
||||
assert hd(bookings).id == booking1.id
|
||||
end
|
||||
end
|
||||
|
||||
test "booking belongs to the user who created it", %{
|
||||
asset: asset,
|
||||
time_slot: time_slot,
|
||||
user: user
|
||||
} do
|
||||
{:ok, booking} =
|
||||
BookingSystem.create_booking(
|
||||
time_slot.id,
|
||||
asset.id,
|
||||
user.id,
|
||||
Date.utc_today(),
|
||||
user.name,
|
||||
user.email,
|
||||
user.phone_number,
|
||||
"test comment"
|
||||
)
|
||||
describe "check_availability/4" do
|
||||
test "returns :available when under public capacity", %{space: space, date: date} do
|
||||
{:ok, booking} =
|
||||
BookingSystem.request_booking(
|
||||
space.id,
|
||||
nil,
|
||||
date,
|
||||
~T[09:00:00],
|
||||
~T[10:00:00],
|
||||
%{name: "User 1", email: "user1@example.com", phone: "", comment: ""}
|
||||
)
|
||||
|
||||
assert booking.user_id == user.id
|
||||
{:ok, _} = BookingSystem.approve_booking(booking.id)
|
||||
|
||||
# Load the booking with the user relationship
|
||||
{:ok, booking_with_user} = Ash.load(booking, :user, authorize?: false)
|
||||
assert booking_with_user.user.id == user.id
|
||||
assert booking_with_user.user.email == user.email
|
||||
assert booking_with_user.user.name == user.name
|
||||
{:ok, status} =
|
||||
BookingSystem.check_availability(
|
||||
space.id,
|
||||
date,
|
||||
~T[09:00:00],
|
||||
~T[10:00:00]
|
||||
)
|
||||
|
||||
assert status == :available
|
||||
end
|
||||
|
||||
test "returns :over_public_capacity when at or over public but under real capacity", %{
|
||||
space: space,
|
||||
date: date
|
||||
} do
|
||||
for i <- 1..2 do
|
||||
{:ok, booking} =
|
||||
BookingSystem.request_booking(
|
||||
space.id,
|
||||
nil,
|
||||
date,
|
||||
~T[09:00:00],
|
||||
~T[10:00:00],
|
||||
%{name: "User #{i}", email: "user#{i}@example.com", phone: "", comment: ""}
|
||||
)
|
||||
|
||||
{:ok, _} = BookingSystem.approve_booking(booking.id)
|
||||
end
|
||||
|
||||
{:ok, status} =
|
||||
BookingSystem.check_availability(
|
||||
space.id,
|
||||
date,
|
||||
~T[09:00:00],
|
||||
~T[10:00:00]
|
||||
)
|
||||
|
||||
assert status == :over_public_capacity
|
||||
end
|
||||
|
||||
test "returns :over_real_capacity when at or over real capacity", %{
|
||||
space: space,
|
||||
date: date
|
||||
} do
|
||||
for i <- 1..3 do
|
||||
{:ok, booking} =
|
||||
BookingSystem.request_booking(
|
||||
space.id,
|
||||
nil,
|
||||
date,
|
||||
~T[09:00:00],
|
||||
~T[10:00:00],
|
||||
%{name: "User #{i}", email: "user#{i}@example.com", phone: "", comment: ""}
|
||||
)
|
||||
|
||||
{:ok, _} = BookingSystem.approve_booking(booking.id)
|
||||
end
|
||||
|
||||
{:ok, status} =
|
||||
BookingSystem.check_availability(
|
||||
space.id,
|
||||
date,
|
||||
~T[09:00:00],
|
||||
~T[10:00:00]
|
||||
)
|
||||
|
||||
assert status == :over_real_capacity
|
||||
end
|
||||
|
||||
test "only counts overlapping bookings", %{space: space, date: date} do
|
||||
{:ok, booking1} =
|
||||
BookingSystem.request_booking(
|
||||
space.id,
|
||||
nil,
|
||||
date,
|
||||
~T[09:00:00],
|
||||
~T[10:00:00],
|
||||
%{name: "User 1", email: "user1@example.com", phone: "", comment: ""}
|
||||
)
|
||||
|
||||
{:ok, _} = BookingSystem.approve_booking(booking1.id)
|
||||
|
||||
{:ok, booking2} =
|
||||
BookingSystem.request_booking(
|
||||
space.id,
|
||||
nil,
|
||||
date,
|
||||
~T[10:00:00],
|
||||
~T[11:00:00],
|
||||
%{name: "User 2", email: "user2@example.com", phone: "", comment: ""}
|
||||
)
|
||||
|
||||
{:ok, _} = BookingSystem.approve_booking(booking2.id)
|
||||
|
||||
{:ok, status} =
|
||||
BookingSystem.check_availability(
|
||||
space.id,
|
||||
date,
|
||||
~T[10:00:00],
|
||||
~T[11:00:00]
|
||||
)
|
||||
|
||||
assert status == :available
|
||||
end
|
||||
|
||||
test "counts partial overlaps", %{space: space, date: date} do
|
||||
for i <- 1..2 do
|
||||
{:ok, booking} =
|
||||
BookingSystem.request_booking(
|
||||
space.id,
|
||||
nil,
|
||||
date,
|
||||
~T[09:00:00],
|
||||
~T[11:00:00],
|
||||
%{name: "User #{i}", email: "user#{i}@example.com", phone: "", comment: ""}
|
||||
)
|
||||
|
||||
{:ok, _} = BookingSystem.approve_booking(booking.id)
|
||||
end
|
||||
|
||||
{:ok, status} =
|
||||
BookingSystem.check_availability(
|
||||
space.id,
|
||||
date,
|
||||
~T[10:00:00],
|
||||
~T[12:00:00]
|
||||
)
|
||||
|
||||
assert status == :over_public_capacity
|
||||
end
|
||||
|
||||
test "does not count pending bookings", %{space: space, date: date} do
|
||||
for i <- 1..3 do
|
||||
{:ok, _booking} =
|
||||
BookingSystem.request_booking(
|
||||
space.id,
|
||||
nil,
|
||||
date,
|
||||
~T[09:00:00],
|
||||
~T[10:00:00],
|
||||
%{name: "User #{i}", email: "user#{i}@example.com", phone: "", comment: ""}
|
||||
)
|
||||
end
|
||||
|
||||
{:ok, status} =
|
||||
BookingSystem.check_availability(
|
||||
space.id,
|
||||
date,
|
||||
~T[09:00:00],
|
||||
~T[10:00:00]
|
||||
)
|
||||
|
||||
assert status == :available
|
||||
end
|
||||
|
||||
test "does not count cancelled bookings", %{space: space, date: date} do
|
||||
for i <- 1..3 do
|
||||
{:ok, booking} =
|
||||
BookingSystem.request_booking(
|
||||
space.id,
|
||||
nil,
|
||||
date,
|
||||
~T[09:00:00],
|
||||
~T[10:00:00],
|
||||
%{name: "User #{i}", email: "user#{i}@example.com", phone: "", comment: ""}
|
||||
)
|
||||
|
||||
{:ok, _} = BookingSystem.approve_booking(booking.id)
|
||||
{:ok, _} = BookingSystem.cancel_booking(booking.id, "Test cancellation")
|
||||
end
|
||||
|
||||
{:ok, status} =
|
||||
BookingSystem.check_availability(
|
||||
space.id,
|
||||
date,
|
||||
~T[09:00:00],
|
||||
~T[10:00:00]
|
||||
)
|
||||
|
||||
assert status == :available
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -4,21 +4,116 @@ defmodule SpazioSolazzo.BookingSystem.SpaceTest do
|
|||
|
||||
alias SpazioSolazzo.BookingSystem
|
||||
|
||||
test "can create a space" do
|
||||
assert {:ok, _} =
|
||||
BookingSystem.create_space("Space", "space", "test description")
|
||||
describe "create_space/5" do
|
||||
test "creates a space with all attributes" do
|
||||
assert {:ok, space} =
|
||||
BookingSystem.create_space(
|
||||
"Test Space",
|
||||
"test-space",
|
||||
"test description",
|
||||
10,
|
||||
12
|
||||
)
|
||||
|
||||
assert {:ok, space} = BookingSystem.get_space_by_slug("space")
|
||||
assert space.name == "Test Space"
|
||||
assert space.slug == "test-space"
|
||||
assert space.description == "test description"
|
||||
assert space.public_capacity == 10
|
||||
assert space.real_capacity == 12
|
||||
end
|
||||
|
||||
assert space.slug == "space"
|
||||
test "requires public_capacity to be less than or equal to real_capacity" do
|
||||
assert {:error, error} =
|
||||
BookingSystem.create_space(
|
||||
"Invalid Space",
|
||||
"invalid",
|
||||
"description",
|
||||
15,
|
||||
10
|
||||
)
|
||||
|
||||
error_messages = Ash.Error.error_descriptions(error)
|
||||
assert String.contains?(error_messages, "must be less than or equal to real_capacity")
|
||||
end
|
||||
|
||||
test "allows public_capacity to equal real_capacity" do
|
||||
assert {:ok, space} =
|
||||
BookingSystem.create_space(
|
||||
"Equal Space",
|
||||
"equal",
|
||||
"description",
|
||||
10,
|
||||
10
|
||||
)
|
||||
|
||||
assert space.public_capacity == 10
|
||||
assert space.real_capacity == 10
|
||||
end
|
||||
|
||||
test "requires positive capacity values" do
|
||||
assert {:error, error} =
|
||||
BookingSystem.create_space(
|
||||
"Zero Space",
|
||||
"zero",
|
||||
"description",
|
||||
-1,
|
||||
5
|
||||
)
|
||||
|
||||
error_messages = Ash.Error.error_descriptions(error)
|
||||
assert String.contains?(error_messages, "must be greater than 0")
|
||||
end
|
||||
end
|
||||
|
||||
test "can't create two spaces with same name and slug" do
|
||||
assert {:ok, _} = BookingSystem.create_space("Space", "space", "test description")
|
||||
assert {:error, error} = BookingSystem.create_space("Space", "space", "test description")
|
||||
describe "get_space_by_slug/1" do
|
||||
test "retrieves space by slug" do
|
||||
{:ok, _} =
|
||||
BookingSystem.create_space("Space", "test-slug", "test description", 5, 5)
|
||||
|
||||
error_messages = Ash.Error.error_descriptions(error)
|
||||
assert {:ok, space} = BookingSystem.get_space_by_slug("test-slug")
|
||||
|
||||
assert String.contains?(error_messages, "has already been")
|
||||
assert space.slug == "test-slug"
|
||||
assert space.name == "Space"
|
||||
end
|
||||
|
||||
test "returns error when space not found" do
|
||||
assert {:error, _} = BookingSystem.get_space_by_slug("nonexistent")
|
||||
end
|
||||
end
|
||||
|
||||
describe "space uniqueness" do
|
||||
test "can't create two spaces with same slug" do
|
||||
assert {:ok, _} =
|
||||
BookingSystem.create_space("Space 1", "same-slug", "description 1", 5, 5)
|
||||
|
||||
assert {:error, error} =
|
||||
BookingSystem.create_space("Space 2", "same-slug", "description 2", 10, 10)
|
||||
|
||||
error_messages = Ash.Error.error_descriptions(error)
|
||||
|
||||
assert String.contains?(error_messages, "has already been")
|
||||
end
|
||||
|
||||
test "can't create two spaces with same name" do
|
||||
assert {:ok, _} =
|
||||
BookingSystem.create_space("Same Name", "slug-1", "description 1", 5, 5)
|
||||
|
||||
assert {:error, error} =
|
||||
BookingSystem.create_space("Same Name", "slug-2", "description 2", 10, 10)
|
||||
|
||||
error_messages = Ash.Error.error_descriptions(error)
|
||||
|
||||
assert String.contains?(error_messages, "has already been")
|
||||
end
|
||||
|
||||
test "can create spaces with different names and slugs" do
|
||||
assert {:ok, space1} =
|
||||
BookingSystem.create_space("Space 1", "slug-1", "description 1", 5, 5)
|
||||
|
||||
assert {:ok, space2} =
|
||||
BookingSystem.create_space("Space 2", "slug-2", "description 2", 10, 10)
|
||||
|
||||
assert space1.id != space2.id
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -5,65 +5,261 @@ defmodule SpazioSolazzo.BookingSystem.TimeSlotTemplateTest do
|
|||
alias SpazioSolazzo.BookingSystem
|
||||
|
||||
setup do
|
||||
{:ok, space} = BookingSystem.create_space("Test", "test", "description")
|
||||
{:ok, space} =
|
||||
BookingSystem.create_space(
|
||||
"Test Space",
|
||||
"test-space",
|
||||
"Test description",
|
||||
10,
|
||||
12
|
||||
)
|
||||
|
||||
%{space: space}
|
||||
end
|
||||
|
||||
test "prevents overlapping time slot templates for same space", %{space: space} do
|
||||
assert {:ok, _} =
|
||||
BookingSystem.create_time_slot_template(
|
||||
~T[09:00:00],
|
||||
~T[12:00:00],
|
||||
:monday,
|
||||
space.id
|
||||
)
|
||||
describe "create_time_slot_template/4" do
|
||||
test "creates a time slot template successfully", %{space: space} do
|
||||
assert {:ok, slot} =
|
||||
BookingSystem.create_time_slot_template(
|
||||
~T[09:00:00],
|
||||
~T[10:00:00],
|
||||
:monday,
|
||||
space.id
|
||||
)
|
||||
|
||||
assert {:error, changeset} =
|
||||
BookingSystem.create_time_slot_template(
|
||||
~T[11:00:00],
|
||||
~T[13:00:00],
|
||||
:monday,
|
||||
space.id
|
||||
)
|
||||
assert slot.start_time == ~T[09:00:00]
|
||||
assert slot.end_time == ~T[10:00:00]
|
||||
assert slot.day_of_week == :monday
|
||||
assert slot.space_id == space.id
|
||||
end
|
||||
|
||||
assert Ash.Error.error_descriptions(changeset.errors) =~ "overlaps"
|
||||
test "creates templates for all days of the week", %{space: space} do
|
||||
days = [:monday, :tuesday, :wednesday, :thursday, :friday, :saturday, :sunday]
|
||||
|
||||
for day <- days do
|
||||
assert {:ok, slot} =
|
||||
BookingSystem.create_time_slot_template(
|
||||
~T[09:00:00],
|
||||
~T[10:00:00],
|
||||
day,
|
||||
space.id
|
||||
)
|
||||
|
||||
assert slot.day_of_week == day
|
||||
end
|
||||
end
|
||||
|
||||
test "prevents overlapping time slots on same day", %{space: space} do
|
||||
assert {:ok, _slot1} =
|
||||
BookingSystem.create_time_slot_template(
|
||||
~T[09:00:00],
|
||||
~T[11:00:00],
|
||||
:monday,
|
||||
space.id
|
||||
)
|
||||
|
||||
assert {:error, error} =
|
||||
BookingSystem.create_time_slot_template(
|
||||
~T[10:00:00],
|
||||
~T[12:00:00],
|
||||
:monday,
|
||||
space.id
|
||||
)
|
||||
|
||||
error_messages = Ash.Error.error_descriptions(error)
|
||||
assert String.contains?(error_messages, "overlaps with existing time slot")
|
||||
end
|
||||
|
||||
test "allows same time slot on different days", %{space: space} do
|
||||
assert {:ok, _slot1} =
|
||||
BookingSystem.create_time_slot_template(
|
||||
~T[09:00:00],
|
||||
~T[11:00:00],
|
||||
:monday,
|
||||
space.id
|
||||
)
|
||||
|
||||
assert {:ok, slot2} =
|
||||
BookingSystem.create_time_slot_template(
|
||||
~T[09:00:00],
|
||||
~T[11:00:00],
|
||||
:tuesday,
|
||||
space.id
|
||||
)
|
||||
|
||||
assert slot2.day_of_week == :tuesday
|
||||
end
|
||||
|
||||
test "allows adjacent time slots on same day", %{space: space} do
|
||||
assert {:ok, _slot1} =
|
||||
BookingSystem.create_time_slot_template(
|
||||
~T[09:00:00],
|
||||
~T[11:00:00],
|
||||
:monday,
|
||||
space.id
|
||||
)
|
||||
|
||||
assert {:ok, slot2} =
|
||||
BookingSystem.create_time_slot_template(
|
||||
~T[11:00:00],
|
||||
~T[13:00:00],
|
||||
:monday,
|
||||
space.id
|
||||
)
|
||||
|
||||
assert slot2.start_time == ~T[11:00:00]
|
||||
end
|
||||
|
||||
test "rejects invalid day of week", %{space: space} do
|
||||
assert {:error, error} =
|
||||
BookingSystem.create_time_slot_template(
|
||||
~T[09:00:00],
|
||||
~T[10:00:00],
|
||||
:invalid_day,
|
||||
space.id
|
||||
)
|
||||
|
||||
error_messages = Ash.Error.error_descriptions(error)
|
||||
assert String.contains?(error_messages, "atom must be one of")
|
||||
end
|
||||
|
||||
test "rejects end time before start time", %{space: space} do
|
||||
assert {:error, error} =
|
||||
BookingSystem.create_time_slot_template(
|
||||
~T[10:00:00],
|
||||
~T[09:00:00],
|
||||
:monday,
|
||||
space.id
|
||||
)
|
||||
|
||||
error_messages = Ash.Error.error_descriptions(error)
|
||||
assert String.contains?(error_messages, "must be after start time")
|
||||
end
|
||||
|
||||
test "rejects equal start and end times", %{space: space} do
|
||||
assert {:error, error} =
|
||||
BookingSystem.create_time_slot_template(
|
||||
~T[10:00:00],
|
||||
~T[10:00:00],
|
||||
:monday,
|
||||
space.id
|
||||
)
|
||||
|
||||
error_messages = Ash.Error.error_descriptions(error)
|
||||
assert String.contains?(error_messages, "must be after start time")
|
||||
end
|
||||
end
|
||||
|
||||
test "allows non-overlapping time slot templates for same space on the same day", %{
|
||||
space: space
|
||||
} do
|
||||
assert {:ok, _} =
|
||||
BookingSystem.create_time_slot_template(
|
||||
~T[09:00:00],
|
||||
~T[12:00:00],
|
||||
:monday,
|
||||
space.id
|
||||
)
|
||||
describe "get_space_time_slots_by_date/2" do
|
||||
setup %{space: space} do
|
||||
{:ok, monday_morning} =
|
||||
BookingSystem.create_time_slot_template(
|
||||
~T[09:00:00],
|
||||
~T[13:00:00],
|
||||
:monday,
|
||||
space.id
|
||||
)
|
||||
|
||||
assert {:ok, _} =
|
||||
BookingSystem.create_time_slot_template(
|
||||
~T[13:00:00],
|
||||
~T[16:00:00],
|
||||
:monday,
|
||||
space.id
|
||||
)
|
||||
end
|
||||
{:ok, monday_afternoon} =
|
||||
BookingSystem.create_time_slot_template(
|
||||
~T[14:00:00],
|
||||
~T[18:00:00],
|
||||
:monday,
|
||||
space.id
|
||||
)
|
||||
|
||||
test "allows overlapping time slot templates for same space on different days", %{space: space} do
|
||||
assert {:ok, _} =
|
||||
BookingSystem.create_time_slot_template(
|
||||
~T[09:00:00],
|
||||
~T[12:00:00],
|
||||
:monday,
|
||||
space.id
|
||||
)
|
||||
{:ok, tuesday_slot} =
|
||||
BookingSystem.create_time_slot_template(
|
||||
~T[09:00:00],
|
||||
~T[13:00:00],
|
||||
:tuesday,
|
||||
space.id
|
||||
)
|
||||
|
||||
assert {:ok, _} =
|
||||
BookingSystem.create_time_slot_template(
|
||||
~T[09:00:00],
|
||||
~T[12:00:00],
|
||||
:tuesday,
|
||||
space.id
|
||||
)
|
||||
%{
|
||||
monday_morning: monday_morning,
|
||||
monday_afternoon: monday_afternoon,
|
||||
tuesday_slot: tuesday_slot
|
||||
}
|
||||
end
|
||||
|
||||
test "returns time slots for specific date's day of week", %{space: space} do
|
||||
monday_date = ~D[2026-02-02]
|
||||
assert Date.day_of_week(monday_date) == 1
|
||||
|
||||
{:ok, slots} = BookingSystem.get_space_time_slots_by_date(space.id, monday_date)
|
||||
|
||||
assert length(slots) == 2
|
||||
assert Enum.any?(slots, &(&1.start_time == ~T[09:00:00]))
|
||||
assert Enum.any?(slots, &(&1.start_time == ~T[14:00:00]))
|
||||
end
|
||||
|
||||
test "returns different slots for different days", %{space: space} do
|
||||
tuesday_date = ~D[2026-02-03]
|
||||
assert Date.day_of_week(tuesday_date) == 2
|
||||
|
||||
{:ok, slots} = BookingSystem.get_space_time_slots_by_date(space.id, tuesday_date)
|
||||
|
||||
assert length(slots) == 1
|
||||
assert hd(slots).start_time == ~T[09:00:00]
|
||||
end
|
||||
|
||||
test "returns empty list when no slots for that day", %{space: space} do
|
||||
wednesday_date = ~D[2026-02-04]
|
||||
assert Date.day_of_week(wednesday_date) == 3
|
||||
|
||||
{:ok, slots} = BookingSystem.get_space_time_slots_by_date(space.id, wednesday_date)
|
||||
|
||||
assert slots == []
|
||||
end
|
||||
|
||||
test "handles all days of the week correctly", %{space: space} do
|
||||
days_and_dates = [
|
||||
{:monday, ~D[2026-02-02]},
|
||||
{:tuesday, ~D[2026-02-03]},
|
||||
{:wednesday, ~D[2026-02-04]},
|
||||
{:thursday, ~D[2026-02-05]},
|
||||
{:friday, ~D[2026-02-06]},
|
||||
{:saturday, ~D[2026-02-07]},
|
||||
{:sunday, ~D[2026-02-01]}
|
||||
]
|
||||
|
||||
for {day_atom, date} <- days_and_dates do
|
||||
BookingSystem.create_time_slot_template(
|
||||
~T[20:00:00],
|
||||
~T[21:00:00],
|
||||
day_atom,
|
||||
space.id
|
||||
)
|
||||
|
||||
{:ok, slots} = BookingSystem.get_space_time_slots_by_date(space.id, date)
|
||||
assert Enum.any?(slots, &(&1.start_time == ~T[20:00:00]))
|
||||
end
|
||||
end
|
||||
|
||||
test "only returns slots for specified space", %{space: space} do
|
||||
{:ok, other_space} =
|
||||
BookingSystem.create_space(
|
||||
"Other Space",
|
||||
"other-space",
|
||||
"Other description",
|
||||
5,
|
||||
5
|
||||
)
|
||||
|
||||
{:ok, _other_slot} =
|
||||
BookingSystem.create_time_slot_template(
|
||||
~T[20:00:00],
|
||||
~T[22:00:00],
|
||||
:monday,
|
||||
other_space.id
|
||||
)
|
||||
|
||||
monday_date = ~D[2026-02-02]
|
||||
{:ok, slots} = BookingSystem.get_space_time_slots_by_date(space.id, monday_date)
|
||||
|
||||
assert Enum.all?(slots, &(&1.space_id == space.id))
|
||||
refute Enum.any?(slots, &(&1.start_time == ~T[20:00:00]))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
defmodule SpazioSolazzoWeb.BookingControllerTest do
|
||||
use SpazioSolazzoWeb.ConnCase, async: true
|
||||
|
||||
@moduletag :skip
|
||||
|
||||
alias SpazioSolazzo.BookingSystem
|
||||
alias SpazioSolazzo.BookingSystem.Booking.Token
|
||||
|
||||
|
|
@ -8,9 +10,13 @@ defmodule SpazioSolazzoWeb.BookingControllerTest do
|
|||
unique_id = :erlang.unique_integer([:positive, :monotonic])
|
||||
|
||||
{:ok, space} =
|
||||
BookingSystem.create_space("Test #{unique_id}", "test-space-#{unique_id}", "desc")
|
||||
|
||||
{:ok, asset} = BookingSystem.create_asset("Table 1", space.id)
|
||||
BookingSystem.create_space(
|
||||
"Test #{unique_id}",
|
||||
"test-space-#{unique_id}",
|
||||
"desc",
|
||||
10,
|
||||
12
|
||||
)
|
||||
|
||||
{:ok, time_slot} =
|
||||
BookingSystem.create_time_slot_template(
|
||||
|
|
@ -22,22 +28,23 @@ defmodule SpazioSolazzoWeb.BookingControllerTest do
|
|||
|
||||
user = register_user("test@example.com", "Test User", "+1234567890")
|
||||
|
||||
%{space: space, asset: asset, time_slot: time_slot, user: user}
|
||||
%{space: space, time_slot: time_slot, user: user}
|
||||
end
|
||||
|
||||
describe "cancel/2" do
|
||||
test "first cancel shows success message, not error message", %{
|
||||
conn: conn,
|
||||
asset: asset,
|
||||
time_slot: time_slot,
|
||||
space: space,
|
||||
time_slot: _time_slot,
|
||||
user: user
|
||||
} do
|
||||
{:ok, booking} =
|
||||
BookingSystem.create_booking(
|
||||
time_slot.id,
|
||||
asset.id,
|
||||
space.id,
|
||||
user.id,
|
||||
Date.utc_today(),
|
||||
~T[09:00:00],
|
||||
~T[11:00:00],
|
||||
"John",
|
||||
"john@example.com",
|
||||
"+393627384027",
|
||||
|
|
@ -65,16 +72,17 @@ defmodule SpazioSolazzoWeb.BookingControllerTest do
|
|||
|
||||
test "shows error message when booking is already cancelled", %{
|
||||
conn: conn,
|
||||
asset: asset,
|
||||
time_slot: time_slot,
|
||||
space: space,
|
||||
time_slot: _time_slot,
|
||||
user: user
|
||||
} do
|
||||
{:ok, booking} =
|
||||
BookingSystem.create_booking(
|
||||
time_slot.id,
|
||||
asset.id,
|
||||
space.id,
|
||||
user.id,
|
||||
Date.utc_today(),
|
||||
~T[09:00:00],
|
||||
~T[11:00:00],
|
||||
"John",
|
||||
"john@example.com",
|
||||
"+393627384027",
|
||||
|
|
@ -82,7 +90,7 @@ defmodule SpazioSolazzoWeb.BookingControllerTest do
|
|||
)
|
||||
|
||||
# Cancel the booking first time
|
||||
{:ok, _cancelled_booking} = BookingSystem.cancel_booking(booking)
|
||||
{:ok, _cancelled_booking} = BookingSystem.cancel_booking(booking.id, "Test cancellation")
|
||||
|
||||
# Generate a cancel token for the already-cancelled booking
|
||||
cancel_token = Token.generate_customer_cancel_token(booking.id)
|
||||
|
|
|
|||
|
|
@ -1,323 +0,0 @@
|
|||
defmodule SpazioSolazzoWeb.BookingLive.AssetBookingTest do
|
||||
use SpazioSolazzoWeb.ConnCase
|
||||
import Phoenix.LiveViewTest
|
||||
|
||||
alias SpazioSolazzo.BookingSystem
|
||||
|
||||
setup %{conn: conn} do
|
||||
{:ok, space} = BookingSystem.create_space("TestSpace", "test-space", "Test description")
|
||||
{:ok, asset} = BookingSystem.create_asset("Test Asset", space.id)
|
||||
|
||||
today = Date.utc_today()
|
||||
day_of_week = SpazioSolazzo.DateExt.day_of_week_atom(today)
|
||||
|
||||
{:ok, slot} =
|
||||
BookingSystem.create_time_slot_template(
|
||||
~T[09:00:00],
|
||||
~T[10:00:00],
|
||||
day_of_week,
|
||||
space.id
|
||||
)
|
||||
|
||||
user = register_user("test@example.com", "Test User", "+1234567890")
|
||||
conn = log_in_user(conn, user)
|
||||
|
||||
%{space: space, asset: asset, slot: slot, conn: conn}
|
||||
end
|
||||
|
||||
describe "AssetBooking mount" do
|
||||
test "renders asset booking page with available time slots", %{
|
||||
conn: conn,
|
||||
space: space,
|
||||
asset: asset
|
||||
} do
|
||||
{:ok, view, html} = live(conn, ~p"/book/asset/#{asset.id}")
|
||||
|
||||
assert html =~ space.name
|
||||
assert html =~ asset.name
|
||||
assert html =~ "Available Time Slots"
|
||||
assert has_element?(view, "button[phx-click='select_slot']")
|
||||
assert has_element?(view, "#booking-calendar")
|
||||
end
|
||||
|
||||
test "displays calendar with current month", %{conn: conn, asset: asset} do
|
||||
{:ok, view, html} = live(conn, ~p"/book/asset/#{asset.id}")
|
||||
|
||||
today = Date.utc_today()
|
||||
month_name = Calendar.strftime(today, "%B %Y")
|
||||
|
||||
assert html =~ month_name
|
||||
assert has_element?(view, ".calendar-container")
|
||||
end
|
||||
|
||||
test "displays back button to space landing page", %{conn: conn, asset: asset, space: space} do
|
||||
{:ok, _view, html} = live(conn, ~p"/book/asset/#{asset.id}")
|
||||
|
||||
assert html =~ "Back to #{space.name}"
|
||||
assert html =~ "/#{space.slug}"
|
||||
end
|
||||
|
||||
test "redirects when asset not found", %{conn: conn} do
|
||||
assert {:error, {:live_redirect, %{to: "/", flash: %{"error" => "Asset not found"}}}} =
|
||||
live(conn, ~p"/book/asset/00000000-0000-0000-0000-000000000000")
|
||||
end
|
||||
end
|
||||
|
||||
describe "AssetBooking time slot selection" do
|
||||
test "opens booking modal when clicking a time slot", %{conn: conn, asset: asset} do
|
||||
{:ok, view, _html} = live(conn, ~p"/book/asset/#{asset.id}")
|
||||
|
||||
view
|
||||
|> element("button[phx-click='select_slot']")
|
||||
|> render_click()
|
||||
|
||||
assert has_element?(view, "#booking-modal")
|
||||
assert has_element?(view, "textarea[name='customer_comment']")
|
||||
end
|
||||
end
|
||||
|
||||
describe "AssetBooking full booking flow" do
|
||||
test "completes full booking flow", %{conn: conn, asset: asset} do
|
||||
{:ok, view, _html} = live(conn, ~p"/book/asset/#{asset.id}")
|
||||
|
||||
view
|
||||
|> element("button[phx-click='select_slot']")
|
||||
|> render_click()
|
||||
|
||||
assert has_element?(view, "#booking-modal")
|
||||
|
||||
view
|
||||
|> element("#booking-form")
|
||||
|> render_submit(%{
|
||||
"customer_name" => "Test User",
|
||||
"customer_phone" => "+1234567890",
|
||||
"customer_comment" => "test comment"
|
||||
})
|
||||
|
||||
assert has_element?(view, "#success-modal")
|
||||
|
||||
assert {:ok, [booking]} =
|
||||
BookingSystem.list_active_asset_bookings_by_date(asset.id, Date.utc_today())
|
||||
|
||||
assert booking.customer_email == "test@example.com"
|
||||
assert booking.customer_name == "Test User"
|
||||
assert booking.customer_phone == "+1234567890"
|
||||
assert booking.customer_comment == "test comment"
|
||||
end
|
||||
end
|
||||
|
||||
describe "AssetBooking cancellation" do
|
||||
test "cancels booking flow", %{conn: conn, asset: asset} do
|
||||
{:ok, view, _html} = live(conn, ~p"/book/asset/#{asset.id}")
|
||||
|
||||
view
|
||||
|> element("button[phx-click='select_slot']")
|
||||
|> render_click()
|
||||
|
||||
assert has_element?(view, "#booking-modal")
|
||||
|
||||
view
|
||||
|> element("button", "Cancel")
|
||||
|> render_click()
|
||||
|
||||
refute has_element?(view, "#booking-form")
|
||||
end
|
||||
end
|
||||
|
||||
describe "AssetBooking date selection" do
|
||||
test "updates available time slots when selecting date from calendar", %{
|
||||
conn: conn,
|
||||
asset: asset,
|
||||
space: space
|
||||
} do
|
||||
{:ok, view, _html} = live(conn, ~p"/book/asset/#{asset.id}")
|
||||
|
||||
tomorrow = Date.add(Date.utc_today(), 1)
|
||||
tomorrow_day_of_week = SpazioSolazzo.DateExt.day_of_week_atom(tomorrow)
|
||||
|
||||
{:ok, _slot} =
|
||||
BookingSystem.create_time_slot_template(
|
||||
~T[14:00:00],
|
||||
~T[15:00:00],
|
||||
tomorrow_day_of_week,
|
||||
space.id
|
||||
)
|
||||
|
||||
# Click on a date in the calendar
|
||||
view
|
||||
|> element(
|
||||
"#booking-calendar button[phx-click='select-date'][phx-value-date='#{Date.to_iso8601(tomorrow)}']"
|
||||
)
|
||||
|> render_click()
|
||||
|
||||
assert has_element?(view, "button[phx-click='select_slot']")
|
||||
end
|
||||
|
||||
test "prevents selection of past dates", %{conn: conn, asset: asset} do
|
||||
{:ok, view, _html} = live(conn, ~p"/book/asset/#{asset.id}")
|
||||
|
||||
yesterday = Date.add(Date.utc_today(), -1)
|
||||
|
||||
# Past dates should be disabled
|
||||
assert has_element?(
|
||||
view,
|
||||
"#booking-calendar button[disabled][phx-value-date='#{Date.to_iso8601(yesterday)}']"
|
||||
)
|
||||
end
|
||||
|
||||
test "displays selected date in the time slots section", %{conn: conn, asset: asset} do
|
||||
{:ok, view, _html} = live(conn, ~p"/book/asset/#{asset.id}")
|
||||
|
||||
today = Date.utc_today()
|
||||
formatted_date = SpazioSolazzo.CalendarExt.format_date(today)
|
||||
|
||||
assert has_element?(view, ".time-slots-wrapper", formatted_date)
|
||||
end
|
||||
end
|
||||
|
||||
describe "AssetBooking calendar navigation" do
|
||||
test "navigates to next month", %{conn: conn, asset: asset} do
|
||||
{:ok, view, _html} = live(conn, ~p"/book/asset/#{asset.id}")
|
||||
|
||||
current_month = Date.utc_today() |> Date.beginning_of_month()
|
||||
next_month = Date.shift(current_month, month: 1)
|
||||
next_month_name = Calendar.strftime(next_month, "%B %Y")
|
||||
|
||||
view
|
||||
|> element("#booking-calendar button[phx-click='next-month']")
|
||||
|> render_click()
|
||||
|
||||
assert has_element?(view, ".calendar-container", next_month_name)
|
||||
end
|
||||
|
||||
test "navigates to previous month", %{conn: conn, asset: asset} do
|
||||
{:ok, view, _html} = live(conn, ~p"/book/asset/#{asset.id}")
|
||||
|
||||
current_month = Date.utc_today() |> Date.beginning_of_month()
|
||||
prev_month = Date.shift(current_month, month: -1)
|
||||
prev_month_name = Calendar.strftime(prev_month, "%B %Y")
|
||||
|
||||
view
|
||||
|> element("#booking-calendar button[phx-click='prev-month']")
|
||||
|> render_click()
|
||||
|
||||
assert has_element?(view, ".calendar-container", prev_month_name)
|
||||
end
|
||||
|
||||
test "only displays days from current viewing month", %{conn: conn, asset: asset} do
|
||||
{:ok, _view, html} = live(conn, ~p"/book/asset/#{asset.id}")
|
||||
|
||||
# Calendar should have empty divs for days not in current month
|
||||
assert html =~ ~s(<div class="p-2"></div>)
|
||||
end
|
||||
end
|
||||
|
||||
describe "AssetBooking without phone number" do
|
||||
setup %{conn: conn} do
|
||||
# Create a separate connection with a user without phone number
|
||||
user = register_user("nophone@example.com", "User Without Phone", nil)
|
||||
conn = log_in_user(conn, user)
|
||||
|
||||
%{conn: conn}
|
||||
end
|
||||
|
||||
test "user without phone number can view booking form", %{conn: conn, asset: asset} do
|
||||
{:ok, view, _html} = live(conn, ~p"/book/asset/#{asset.id}")
|
||||
|
||||
view
|
||||
|> element("button[phx-click='select_slot']")
|
||||
|> render_click()
|
||||
|
||||
assert has_element?(view, "#booking-modal")
|
||||
assert has_element?(view, "input[name='customer_name']")
|
||||
assert has_element?(view, "input[name='customer_phone']")
|
||||
assert has_element?(view, "textarea[name='customer_comment']")
|
||||
end
|
||||
|
||||
test "user without phone number can create booking without providing phone", %{
|
||||
conn: conn,
|
||||
asset: asset
|
||||
} do
|
||||
{:ok, view, _html} = live(conn, ~p"/book/asset/#{asset.id}")
|
||||
|
||||
view
|
||||
|> element("button[phx-click='select_slot']")
|
||||
|> render_click()
|
||||
|
||||
assert has_element?(view, "#booking-modal")
|
||||
|
||||
# Submit booking with name but no phone
|
||||
view
|
||||
|> element("#booking-form")
|
||||
|> render_submit(%{
|
||||
"customer_name" => "User Without Phone",
|
||||
"customer_phone" => "",
|
||||
"customer_comment" => "test comment"
|
||||
})
|
||||
|
||||
assert has_element?(view, "#success-modal")
|
||||
|
||||
assert {:ok, [booking]} =
|
||||
BookingSystem.list_active_asset_bookings_by_date(asset.id, Date.utc_today())
|
||||
|
||||
assert booking.customer_email == "nophone@example.com"
|
||||
assert booking.customer_name == "User Without Phone"
|
||||
assert booking.customer_phone == nil or booking.customer_phone == ""
|
||||
assert booking.customer_comment == "test comment"
|
||||
end
|
||||
|
||||
test "user without phone number can edit name in booking form", %{
|
||||
conn: conn,
|
||||
asset: asset
|
||||
} do
|
||||
{:ok, view, _html} = live(conn, ~p"/book/asset/#{asset.id}")
|
||||
|
||||
view
|
||||
|> element("button[phx-click='select_slot']")
|
||||
|> render_click()
|
||||
|
||||
# Change the name
|
||||
view
|
||||
|> element("#booking-form")
|
||||
|> render_submit(%{
|
||||
"customer_name" => "Different Name",
|
||||
"customer_phone" => "",
|
||||
"customer_comment" => ""
|
||||
})
|
||||
|
||||
assert has_element?(view, "#success-modal")
|
||||
|
||||
assert {:ok, [booking]} =
|
||||
BookingSystem.list_active_asset_bookings_by_date(asset.id, Date.utc_today())
|
||||
|
||||
assert booking.customer_name == "Different Name"
|
||||
end
|
||||
|
||||
test "user without phone number can optionally add phone during booking", %{
|
||||
conn: conn,
|
||||
asset: asset
|
||||
} do
|
||||
{:ok, view, _html} = live(conn, ~p"/book/asset/#{asset.id}")
|
||||
|
||||
view
|
||||
|> element("button[phx-click='select_slot']")
|
||||
|> render_click()
|
||||
|
||||
# Add phone number during booking
|
||||
view
|
||||
|> element("#booking-form")
|
||||
|> render_submit(%{
|
||||
"customer_name" => "User Without Phone",
|
||||
"customer_phone" => "+39 123 456 789",
|
||||
"customer_comment" => ""
|
||||
})
|
||||
|
||||
assert has_element?(view, "#success-modal")
|
||||
|
||||
assert {:ok, [booking]} =
|
||||
BookingSystem.list_active_asset_bookings_by_date(asset.id, Date.utc_today())
|
||||
|
||||
assert booking.customer_phone == "+39 123 456 789"
|
||||
end
|
||||
end
|
||||
end
|
||||
658
test/spazio_solazzo_web/live/booking_live/space_booking_test.exs
Normal file
658
test/spazio_solazzo_web/live/booking_live/space_booking_test.exs
Normal file
|
|
@ -0,0 +1,658 @@
|
|||
defmodule SpazioSolazzoWeb.BookingLive.SpaceBookingTest do
|
||||
use SpazioSolazzoWeb.ConnCase
|
||||
import Phoenix.LiveViewTest
|
||||
import SpazioSolazzo.AuthHelpers
|
||||
|
||||
alias SpazioSolazzo.BookingSystem
|
||||
|
||||
setup %{conn: conn} do
|
||||
{:ok, space} =
|
||||
BookingSystem.create_space(
|
||||
"Test Space",
|
||||
"test-space",
|
||||
"Test description",
|
||||
2,
|
||||
3
|
||||
)
|
||||
|
||||
today = Date.utc_today()
|
||||
day_of_week = day_of_week_atom(today)
|
||||
|
||||
{:ok, slot1} =
|
||||
BookingSystem.create_time_slot_template(
|
||||
~T[09:00:00],
|
||||
~T[13:00:00],
|
||||
day_of_week,
|
||||
space.id
|
||||
)
|
||||
|
||||
{:ok, slot2} =
|
||||
BookingSystem.create_time_slot_template(
|
||||
~T[14:00:00],
|
||||
~T[18:00:00],
|
||||
day_of_week,
|
||||
space.id
|
||||
)
|
||||
|
||||
user = register_user("testuser@example.com", "Test User", "+39 1234567890")
|
||||
conn = log_in_user(conn, user)
|
||||
|
||||
%{conn: conn, space: space, slot1: slot1, slot2: slot2, today: today, user: user}
|
||||
end
|
||||
|
||||
defp day_of_week_atom(date) do
|
||||
case Date.day_of_week(date) do
|
||||
1 -> :monday
|
||||
2 -> :tuesday
|
||||
3 -> :wednesday
|
||||
4 -> :thursday
|
||||
5 -> :friday
|
||||
6 -> :saturday
|
||||
7 -> :sunday
|
||||
end
|
||||
end
|
||||
|
||||
describe "SpaceBooking mount" do
|
||||
test "renders space booking page with available time slots", %{
|
||||
conn: conn,
|
||||
space: space
|
||||
} do
|
||||
{:ok, view, html} = live(conn, ~p"/book/space/#{space.slug}")
|
||||
|
||||
assert html =~ space.name
|
||||
assert html =~ space.description
|
||||
assert html =~ "Available Time Slots"
|
||||
assert has_element?(view, "button[phx-click='select_slot']")
|
||||
assert has_element?(view, "#booking-calendar")
|
||||
end
|
||||
|
||||
test "displays calendar with current month", %{conn: conn, space: space} do
|
||||
{:ok, view, html} = live(conn, ~p"/book/space/#{space.slug}")
|
||||
|
||||
today = Date.utc_today()
|
||||
month_name = Calendar.strftime(today, "%B %Y")
|
||||
|
||||
assert html =~ month_name
|
||||
assert has_element?(view, ".calendar-container")
|
||||
end
|
||||
|
||||
test "displays back button to home page", %{conn: conn, space: space} do
|
||||
{:ok, _view, html} = live(conn, ~p"/book/space/#{space.slug}")
|
||||
|
||||
assert html =~ "Back to #{space.name}"
|
||||
assert html =~ "/#{space.slug}"
|
||||
end
|
||||
|
||||
test "redirects when space not found", %{conn: conn} do
|
||||
assert {:error, {:live_redirect, %{to: "/", flash: %{"error" => "Space not found"}}}} =
|
||||
live(conn, ~p"/book/space/nonexistent-space")
|
||||
end
|
||||
|
||||
test "shows message when no time slots available", %{conn: conn} do
|
||||
{:ok, empty_space} =
|
||||
BookingSystem.create_space(
|
||||
"Empty Space",
|
||||
"empty-space",
|
||||
"No slots",
|
||||
5,
|
||||
5
|
||||
)
|
||||
|
||||
{:ok, _view, html} = live(conn, ~p"/book/space/#{empty_space.slug}")
|
||||
|
||||
assert html =~ "No time slots available for this date"
|
||||
end
|
||||
end
|
||||
|
||||
describe "SpaceBooking calendar navigation" do
|
||||
test "navigates to previous month", %{conn: conn, space: space} do
|
||||
{:ok, view, _html} = live(conn, ~p"/book/space/#{space.slug}")
|
||||
|
||||
today = Date.utc_today()
|
||||
current_month = Calendar.strftime(today, "%B %Y")
|
||||
|
||||
prev_month =
|
||||
today
|
||||
|> Date.shift(month: -1)
|
||||
|> Calendar.strftime("%B %Y")
|
||||
|
||||
html = view |> element("button[phx-click='prev-month']") |> render_click()
|
||||
|
||||
assert html =~ prev_month
|
||||
refute html =~ current_month
|
||||
end
|
||||
|
||||
test "navigates to next month", %{conn: conn, space: space} do
|
||||
{:ok, view, _html} = live(conn, ~p"/book/space/#{space.slug}")
|
||||
|
||||
today = Date.utc_today()
|
||||
current_month = Calendar.strftime(today, "%B %Y")
|
||||
|
||||
next_month =
|
||||
today
|
||||
|> Date.shift(month: 1)
|
||||
|> Calendar.strftime("%B %Y")
|
||||
|
||||
html = view |> element("button[phx-click='next-month']") |> render_click()
|
||||
|
||||
assert html =~ next_month
|
||||
refute html =~ current_month
|
||||
end
|
||||
end
|
||||
|
||||
describe "SpaceBooking date selection" do
|
||||
test "updates time slots when selecting a different date", %{conn: conn, space: space} do
|
||||
monday_date = ~D[2026-02-02]
|
||||
|
||||
{:ok, _monday_slot} =
|
||||
BookingSystem.create_time_slot_template(
|
||||
~T[20:00:00],
|
||||
~T[22:00:00],
|
||||
:monday,
|
||||
space.id
|
||||
)
|
||||
|
||||
{:ok, view, _html} = live(conn, ~p"/book/space/#{space.slug}")
|
||||
|
||||
# Click on a date button in the calendar
|
||||
view
|
||||
|> element("button[phx-value-date='#{Date.to_iso8601(monday_date)}']")
|
||||
|> render_click()
|
||||
|
||||
html = render(view)
|
||||
|
||||
assert html =~ "20:00"
|
||||
assert html =~ "22:00"
|
||||
end
|
||||
|
||||
test "shows selected date in formatted string", %{conn: conn, space: space} do
|
||||
{:ok, view, _html} = live(conn, ~p"/book/space/#{space.slug}")
|
||||
|
||||
today = Date.utc_today()
|
||||
formatted_date = Calendar.strftime(today, "%A, %B %d, %Y")
|
||||
|
||||
html = render(view)
|
||||
|
||||
assert html =~ formatted_date
|
||||
end
|
||||
|
||||
test "does not allow selecting past dates", %{conn: conn, space: space} do
|
||||
{:ok, view, _html} = live(conn, ~p"/book/space/#{space.slug}")
|
||||
|
||||
# Navigate to previous month (January 2026), where all dates are in the past
|
||||
view
|
||||
|> element("button[phx-click='prev-month']")
|
||||
|> render_click()
|
||||
|
||||
# Check that there's at least one disabled button (past date)
|
||||
html = render(view)
|
||||
assert html =~ "disabled"
|
||||
|
||||
# Verify that clicking a past date doesn't change the selected date
|
||||
# by checking we can't click on January 15th (a past date)
|
||||
refute has_element?(view, "button[phx-click='select-date'][phx-value-date='2026-01-15']")
|
||||
end
|
||||
end
|
||||
|
||||
describe "SpaceBooking time slot display" do
|
||||
test "displays available time slots with correct styling", %{conn: conn, space: space} do
|
||||
{:ok, _view, html} = live(conn, ~p"/book/space/#{space.slug}")
|
||||
|
||||
assert html =~ "09:00"
|
||||
assert html =~ "13:00"
|
||||
assert html =~ "14:00"
|
||||
assert html =~ "18:00"
|
||||
assert html =~ "Available - Request Booking"
|
||||
end
|
||||
|
||||
test "shows high demand label for slots over public capacity", %{
|
||||
conn: conn,
|
||||
space: space,
|
||||
today: today
|
||||
} do
|
||||
for i <- 1..2 do
|
||||
{:ok, booking} =
|
||||
BookingSystem.request_booking(
|
||||
space.id,
|
||||
nil,
|
||||
today,
|
||||
~T[09:00:00],
|
||||
~T[13:00:00],
|
||||
%{name: "User #{i}", email: "user#{i}@example.com", phone: "", comment: ""}
|
||||
)
|
||||
|
||||
{:ok, _} = BookingSystem.approve_booking(booking.id)
|
||||
end
|
||||
|
||||
{:ok, _view, html} = live(conn, ~p"/book/space/#{space.slug}")
|
||||
|
||||
assert html =~ "High Demand - Join Waitlist"
|
||||
end
|
||||
|
||||
test "hides slots over real capacity", %{conn: conn, space: space, today: today} do
|
||||
for i <- 1..3 do
|
||||
{:ok, booking} =
|
||||
BookingSystem.request_booking(
|
||||
space.id,
|
||||
nil,
|
||||
today,
|
||||
~T[09:00:00],
|
||||
~T[13:00:00],
|
||||
%{name: "User #{i}", email: "user#{i}@example.com", phone: "", comment: ""}
|
||||
)
|
||||
|
||||
{:ok, _} = BookingSystem.approve_booking(booking.id)
|
||||
end
|
||||
|
||||
{:ok, view, _html} = live(conn, ~p"/book/space/#{space.slug}")
|
||||
|
||||
html = render(view)
|
||||
|
||||
assert html =~ "14:00"
|
||||
refute html =~ "09:00"
|
||||
end
|
||||
end
|
||||
|
||||
describe "SpaceBooking modal interaction" do
|
||||
test "opens booking modal when clicking a time slot", %{
|
||||
conn: conn,
|
||||
space: space,
|
||||
slot1: slot1,
|
||||
user: user
|
||||
} do
|
||||
{:ok, view, _html} = live(conn, ~p"/book/space/#{space.slug}")
|
||||
|
||||
view
|
||||
|> element("button[phx-click='select_slot'][phx-value-time_slot_id='#{slot1.id}']")
|
||||
|> render_click()
|
||||
|
||||
assert has_element?(view, "#booking-modal")
|
||||
assert has_element?(view, "input[name='customer_name']")
|
||||
# Authenticated users see their email displayed, not an input field
|
||||
assert render(view) =~ to_string(user.email)
|
||||
assert has_element?(view, "input[name='customer_phone']")
|
||||
assert has_element?(view, "textarea[name='customer_comment']")
|
||||
end
|
||||
|
||||
test "closes modal when clicking cancel", %{conn: conn, space: space, slot1: slot1} do
|
||||
{:ok, view, _html} = live(conn, ~p"/book/space/#{space.slug}")
|
||||
|
||||
view
|
||||
|> element("button[phx-click='select_slot'][phx-value-time_slot_id='#{slot1.id}']")
|
||||
|> render_click()
|
||||
|
||||
assert has_element?(view, "#booking-modal")
|
||||
|
||||
view
|
||||
|> element("button", "Cancel")
|
||||
|> render_click()
|
||||
|
||||
refute has_element?(view, "#booking-modal")
|
||||
end
|
||||
|
||||
test "shows high demand warning in modal for slots over public capacity", %{
|
||||
conn: conn,
|
||||
space: space,
|
||||
today: today,
|
||||
slot1: slot1
|
||||
} do
|
||||
for i <- 1..2 do
|
||||
{:ok, booking} =
|
||||
BookingSystem.request_booking(
|
||||
space.id,
|
||||
nil,
|
||||
today,
|
||||
~T[09:00:00],
|
||||
~T[13:00:00],
|
||||
%{name: "User #{i}", email: "user#{i}@example.com", phone: "", comment: ""}
|
||||
)
|
||||
|
||||
{:ok, _} = BookingSystem.approve_booking(booking.id)
|
||||
end
|
||||
|
||||
{:ok, view, _html} = live(conn, ~p"/book/space/#{space.slug}")
|
||||
|
||||
html =
|
||||
view
|
||||
|> element("button[phx-click='select_slot'][phx-value-time_slot_id='#{slot1.id}']")
|
||||
|> render_click()
|
||||
|
||||
assert html =~ "High Demand Time Slot"
|
||||
assert html =~ "subject to admin approval"
|
||||
end
|
||||
end
|
||||
|
||||
describe "SpaceBooking submission" do
|
||||
test "completes booking request successfully", %{conn: conn, space: space, slot1: slot1} do
|
||||
{:ok, view, _html} = live(conn, ~p"/book/space/#{space.slug}")
|
||||
|
||||
view
|
||||
|> element("button[phx-click='select_slot'][phx-value-time_slot_id='#{slot1.id}']")
|
||||
|> render_click()
|
||||
|
||||
form_data = %{
|
||||
"customer_name" => "Test User",
|
||||
"customer_email" => "testuser@example.com",
|
||||
"customer_phone" => "+39 1234567890",
|
||||
"customer_comment" => "Test comment"
|
||||
}
|
||||
|
||||
view
|
||||
|> element("#booking-form")
|
||||
|> render_submit(form_data)
|
||||
|
||||
assert has_element?(view, "#success-modal")
|
||||
assert render(view) =~ "Request Submitted!"
|
||||
assert render(view) =~ "pending approval"
|
||||
end
|
||||
|
||||
test "creates booking in database", %{conn: conn, space: space, today: today, slot1: slot1} do
|
||||
{:ok, view, _html} = live(conn, ~p"/book/space/#{space.slug}")
|
||||
|
||||
view
|
||||
|> element("button[phx-click='select_slot'][phx-value-time_slot_id='#{slot1.id}']")
|
||||
|> render_click()
|
||||
|
||||
form_data = %{
|
||||
"customer_name" => "Test User",
|
||||
"customer_email" => "testuser@example.com",
|
||||
"customer_phone" => "+39 1234567890",
|
||||
"customer_comment" => "Test comment"
|
||||
}
|
||||
|
||||
view
|
||||
|> element("#booking-form")
|
||||
|> render_submit(form_data)
|
||||
|
||||
{:ok, bookings} = BookingSystem.list_booking_requests(space.id, nil, today)
|
||||
|
||||
assert length(bookings) == 1
|
||||
booking = hd(bookings)
|
||||
assert booking.customer_name == "Test User"
|
||||
assert booking.customer_email == "testuser@example.com"
|
||||
assert booking.customer_phone == "+39 1234567890"
|
||||
assert booking.customer_comment == "Test comment"
|
||||
assert booking.state == :requested
|
||||
end
|
||||
|
||||
test "validates required fields", %{conn: conn, space: space, slot1: slot1} do
|
||||
{:ok, view, _html} = live(conn, ~p"/book/space/#{space.slug}")
|
||||
|
||||
view
|
||||
|> element("button[phx-click='select_slot'][phx-value-time_slot_id='#{slot1.id}']")
|
||||
|> render_click()
|
||||
|
||||
form_data = %{
|
||||
"customer_name" => "",
|
||||
"customer_email" => "",
|
||||
"customer_phone" => "",
|
||||
"customer_comment" => ""
|
||||
}
|
||||
|
||||
view
|
||||
|> element("#booking-form")
|
||||
|> render_submit(form_data)
|
||||
|
||||
refute has_element?(view, "#success-modal")
|
||||
end
|
||||
|
||||
test "closes success modal when clicking close", %{conn: conn, space: space, slot1: slot1} do
|
||||
{:ok, view, _html} = live(conn, ~p"/book/space/#{space.slug}")
|
||||
|
||||
view
|
||||
|> element("button[phx-click='select_slot'][phx-value-time_slot_id='#{slot1.id}']")
|
||||
|> render_click()
|
||||
|
||||
form_data = %{
|
||||
"customer_name" => "Test User",
|
||||
"customer_email" => "testuser@example.com",
|
||||
"customer_phone" => "",
|
||||
"customer_comment" => ""
|
||||
}
|
||||
|
||||
view
|
||||
|> element("#booking-form")
|
||||
|> render_submit(form_data)
|
||||
|
||||
assert has_element?(view, "#success-modal")
|
||||
|
||||
view
|
||||
|> element("button[phx-click='close_success_modal']")
|
||||
|> render_click()
|
||||
|
||||
refute has_element?(view, "#success-modal")
|
||||
end
|
||||
end
|
||||
|
||||
describe "SpaceBooking real-time updates" do
|
||||
test "updates availability when booking is approved", %{
|
||||
conn: conn,
|
||||
space: space,
|
||||
today: today
|
||||
} do
|
||||
{:ok, view, _html} = live(conn, ~p"/book/space/#{space.slug}")
|
||||
|
||||
initial_html = render(view)
|
||||
assert initial_html =~ "Available - Request Booking"
|
||||
|
||||
{:ok, booking} =
|
||||
BookingSystem.request_booking(
|
||||
space.id,
|
||||
nil,
|
||||
today,
|
||||
~T[09:00:00],
|
||||
~T[13:00:00],
|
||||
%{name: "User 1", email: "user1@example.com", phone: "", comment: ""}
|
||||
)
|
||||
|
||||
{:ok, _} = BookingSystem.approve_booking(booking.id)
|
||||
|
||||
html = render(view)
|
||||
assert html =~ "Available - Request Booking"
|
||||
|
||||
{:ok, booking2} =
|
||||
BookingSystem.request_booking(
|
||||
space.id,
|
||||
nil,
|
||||
today,
|
||||
~T[09:00:00],
|
||||
~T[13:00:00],
|
||||
%{name: "User 2", email: "user2@example.com", phone: "", comment: ""}
|
||||
)
|
||||
|
||||
{:ok, _} = BookingSystem.approve_booking(booking2.id)
|
||||
|
||||
html = render(view)
|
||||
assert html =~ "High Demand - Join Waitlist"
|
||||
end
|
||||
|
||||
test "updates when booking is cancelled", %{conn: conn, space: space, today: today} do
|
||||
{:ok, booking1} =
|
||||
BookingSystem.request_booking(
|
||||
space.id,
|
||||
nil,
|
||||
today,
|
||||
~T[09:00:00],
|
||||
~T[13:00:00],
|
||||
%{name: "User 1", email: "user1@example.com", phone: "", comment: ""}
|
||||
)
|
||||
|
||||
{:ok, _} = BookingSystem.approve_booking(booking1.id)
|
||||
|
||||
{:ok, booking2} =
|
||||
BookingSystem.request_booking(
|
||||
space.id,
|
||||
nil,
|
||||
today,
|
||||
~T[09:00:00],
|
||||
~T[13:00:00],
|
||||
%{name: "User 2", email: "user2@example.com", phone: "", comment: ""}
|
||||
)
|
||||
|
||||
{:ok, _} = BookingSystem.approve_booking(booking2.id)
|
||||
|
||||
{:ok, view, _html} = live(conn, ~p"/book/space/#{space.slug}")
|
||||
|
||||
html = render(view)
|
||||
assert html =~ "High Demand - Join Waitlist"
|
||||
|
||||
{:ok, _} = BookingSystem.cancel_booking(booking1.id, "Test cancellation")
|
||||
|
||||
html = render(view)
|
||||
assert html =~ "Available - Request Booking"
|
||||
end
|
||||
end
|
||||
|
||||
describe "SpaceBooking authenticated user flow" do
|
||||
test "pre-fills user data in booking form", %{
|
||||
conn: conn,
|
||||
space: space,
|
||||
user: user,
|
||||
slot1: slot1
|
||||
} do
|
||||
{:ok, view, _html} = live(conn, ~p"/book/space/#{space.slug}")
|
||||
|
||||
html =
|
||||
view
|
||||
|> element("button[phx-click='select_slot'][phx-value-time_slot_id='#{slot1.id}']")
|
||||
|> render_click()
|
||||
|
||||
assert html =~ user.name
|
||||
assert html =~ to_string(user.email)
|
||||
assert html =~ user.phone_number
|
||||
end
|
||||
|
||||
test "uses authenticated user email for booking", %{
|
||||
slot1: slot1,
|
||||
conn: conn,
|
||||
space: space,
|
||||
user: user,
|
||||
today: today
|
||||
} do
|
||||
{:ok, view, _html} = live(conn, ~p"/book/space/#{space.slug}")
|
||||
|
||||
view
|
||||
|> element("button[phx-click='select_slot'][phx-value-time_slot_id='#{slot1.id}']")
|
||||
|> render_click()
|
||||
|
||||
form_data = %{
|
||||
"customer_name" => "Test User",
|
||||
"customer_phone" => "+39 1234567890",
|
||||
"customer_comment" => "Test"
|
||||
}
|
||||
|
||||
view
|
||||
|> element("#booking-form")
|
||||
|> render_submit(form_data)
|
||||
|
||||
{:ok, bookings} = BookingSystem.list_booking_requests(space.id, nil, today)
|
||||
|
||||
assert length(bookings) == 1
|
||||
booking = hd(bookings)
|
||||
assert to_string(booking.customer_email) == to_string(user.email)
|
||||
assert booking.user_id == user.id
|
||||
end
|
||||
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
|
||||
|
||||
test "shows high demand when public capacity is reached", %{conn: conn} do
|
||||
{:ok, small_space} =
|
||||
BookingSystem.create_space(
|
||||
"Small Space",
|
||||
"small-space",
|
||||
"Limited public capacity",
|
||||
1,
|
||||
2
|
||||
)
|
||||
|
||||
today = Date.utc_today()
|
||||
day_of_week = day_of_week_atom(today)
|
||||
|
||||
{:ok, _slot} =
|
||||
BookingSystem.create_time_slot_template(
|
||||
~T[09:00:00],
|
||||
~T[10:00:00],
|
||||
day_of_week,
|
||||
small_space.id
|
||||
)
|
||||
|
||||
{:ok, booking} =
|
||||
BookingSystem.request_booking(
|
||||
small_space.id,
|
||||
nil,
|
||||
today,
|
||||
~T[09:00:00],
|
||||
~T[10:00:00],
|
||||
%{name: "User 1", email: "user1@example.com", phone: "", comment: ""}
|
||||
)
|
||||
|
||||
{:ok, _} = BookingSystem.approve_booking(booking.id)
|
||||
|
||||
{:ok, _view, html} = live(conn, ~p"/book/space/#{small_space.slug}")
|
||||
|
||||
assert html =~ "High Demand - Join Waitlist"
|
||||
end
|
||||
|
||||
test "handles rapid date changes", %{conn: conn, space: space} do
|
||||
{:ok, view, _html} = live(conn, ~p"/book/space/#{space.slug}")
|
||||
|
||||
dates = [~D[2026-02-02], ~D[2026-02-03], ~D[2026-02-04], ~D[2026-02-02]]
|
||||
|
||||
for date <- dates do
|
||||
view
|
||||
|> element("button[phx-click='select-date'][phx-value-date='#{Date.to_iso8601(date)}']")
|
||||
|> render_click()
|
||||
end
|
||||
|
||||
html = render(view)
|
||||
assert html =~ "Monday, February 02, 2026"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -5,11 +5,9 @@ defmodule SpazioSolazzoWeb.CoworkingLiveTest do
|
|||
alias SpazioSolazzo.BookingSystem
|
||||
|
||||
setup do
|
||||
{:ok, space} = BookingSystem.create_space("CoworkingTest", "coworking", "desc")
|
||||
{:ok, asset1} = BookingSystem.create_asset("Table 1", space.id)
|
||||
{:ok, asset2} = BookingSystem.create_asset("Table 2", space.id)
|
||||
{:ok, space} = BookingSystem.create_space("CoworkingTest", "coworking", "desc", 10, 12)
|
||||
|
||||
%{space: space, assets: [asset1, asset2]}
|
||||
%{space: space}
|
||||
end
|
||||
|
||||
describe "CoworkingLive landing page" do
|
||||
|
|
@ -17,21 +15,13 @@ defmodule SpazioSolazzoWeb.CoworkingLiveTest do
|
|||
{:ok, _view, html} = live(conn, "/coworking")
|
||||
|
||||
assert html =~ space.name
|
||||
assert html =~ "Interactive Floor Plan"
|
||||
assert html =~ "Fiber Internet"
|
||||
end
|
||||
|
||||
test "displays all available assets as selectable cards", %{
|
||||
conn: conn,
|
||||
assets: [asset1, asset2]
|
||||
} do
|
||||
{:ok, view, html} = live(conn, "/coworking")
|
||||
test "has link to space booking page with correct space slug", %{conn: conn, space: space} do
|
||||
{:ok, view, _html} = live(conn, "/coworking")
|
||||
|
||||
assert html =~ asset1.name
|
||||
assert html =~ asset2.name
|
||||
|
||||
assert has_element?(view, "a[href='/book/asset/#{asset1.id}']")
|
||||
assert has_element?(view, "a[href='/book/asset/#{asset2.id}']")
|
||||
assert has_element?(view, "a[href='/book/space/#{space.slug}']")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -5,10 +5,9 @@ defmodule SpazioSolazzoWeb.MeetingLiveTest do
|
|||
alias SpazioSolazzo.BookingSystem
|
||||
|
||||
setup do
|
||||
{:ok, space} = BookingSystem.create_space("MeetingTest", "meeting", "desc")
|
||||
{:ok, asset} = BookingSystem.create_asset("Main Room", space.id)
|
||||
{:ok, space} = BookingSystem.create_space("MeetingTest", "meeting", "desc", 1, 1)
|
||||
|
||||
%{space: space, asset: asset}
|
||||
%{space: space}
|
||||
end
|
||||
|
||||
describe "MeetingLive landing page" do
|
||||
|
|
@ -16,17 +15,15 @@ defmodule SpazioSolazzoWeb.MeetingLiveTest do
|
|||
conn: conn,
|
||||
space: space
|
||||
} do
|
||||
{:ok, view, html} = live(conn, "/meeting")
|
||||
{:ok, _view, html} = live(conn, "/meeting")
|
||||
|
||||
assert html =~ space.name
|
||||
assert html =~ "Book This Room"
|
||||
assert has_element?(view, "h2", "Everything you need to succeed")
|
||||
end
|
||||
|
||||
test "has link to asset booking page with correct asset id", %{conn: conn, asset: asset} do
|
||||
test "has link to space booking page with correct space slug", %{conn: conn, space: space} do
|
||||
{:ok, view, _html} = live(conn, "/meeting")
|
||||
|
||||
assert has_element?(view, "a[href='/book/asset/#{asset.id}']", "Book This Room")
|
||||
assert has_element?(view, "a[href='/book/space/#{space.slug}']")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -5,10 +5,9 @@ defmodule SpazioSolazzoWeb.MusicLiveTest do
|
|||
alias SpazioSolazzo.BookingSystem
|
||||
|
||||
setup do
|
||||
{:ok, space} = BookingSystem.create_space("MusicTest", "music", "desc")
|
||||
{:ok, asset} = BookingSystem.create_asset("Studio", space.id)
|
||||
{:ok, space} = BookingSystem.create_space("MusicTest", "music", "desc", 1, 2)
|
||||
|
||||
%{space: space, asset: asset}
|
||||
%{space: space}
|
||||
end
|
||||
|
||||
describe "MusicLive landing page" do
|
||||
|
|
@ -16,17 +15,15 @@ defmodule SpazioSolazzoWeb.MusicLiveTest do
|
|||
conn: conn,
|
||||
space: space
|
||||
} do
|
||||
{:ok, view, html} = live(conn, "/music")
|
||||
{:ok, _view, html} = live(conn, "/music")
|
||||
|
||||
assert html =~ space.name
|
||||
assert html =~ "Book This Room"
|
||||
assert has_element?(view, "h2", "Jam, Practice, Create")
|
||||
end
|
||||
|
||||
test "has link to asset booking page with correct asset id", %{conn: conn, asset: asset} do
|
||||
test "has link to space booking page with correct space slug", %{conn: conn, space: space} do
|
||||
{:ok, view, _html} = live(conn, "/music")
|
||||
|
||||
assert has_element?(view, "a[href='/book/asset/#{asset.id}']", "Book This Room")
|
||||
assert has_element?(view, "a[href='/book/space/#{space.slug}']")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ defmodule SpazioSolazzoWeb.PageLiveTest do
|
|||
|
||||
setup do
|
||||
for {name, slug} <- [{"Coworking", "coworking"}, {"Meeting", "meeting"}, {"Music", "music"}] do
|
||||
BookingSystem.create_space!(name, slug, "desc")
|
||||
BookingSystem.create_space!(name, slug, "desc", 10, 12)
|
||||
end
|
||||
|
||||
:ok
|
||||
|
|
|
|||
|
|
@ -123,14 +123,15 @@ defmodule SpazioSolazzoWeb.ProfileLiveTest do
|
|||
conn: conn,
|
||||
user: user
|
||||
} do
|
||||
{_space, asset, time_slot} = create_booking_fixtures()
|
||||
{space, _time_slot} = create_booking_fixtures()
|
||||
|
||||
{:ok, booking} =
|
||||
BookingSystem.create_booking(
|
||||
time_slot.id,
|
||||
asset.id,
|
||||
space.id,
|
||||
user.id,
|
||||
Date.utc_today(),
|
||||
~T[09:00:00],
|
||||
~T[11:00:00],
|
||||
"Test User",
|
||||
"test@example.com",
|
||||
"+1234567890",
|
||||
|
|
@ -158,14 +159,15 @@ defmodule SpazioSolazzoWeb.ProfileLiveTest do
|
|||
conn: conn,
|
||||
user: user
|
||||
} do
|
||||
{_space, asset, time_slot} = create_booking_fixtures()
|
||||
{space, _time_slot} = create_booking_fixtures()
|
||||
|
||||
{:ok, booking} =
|
||||
BookingSystem.create_booking(
|
||||
time_slot.id,
|
||||
asset.id,
|
||||
space.id,
|
||||
user.id,
|
||||
Date.utc_today(),
|
||||
~T[09:00:00],
|
||||
~T[11:00:00],
|
||||
"Test User",
|
||||
"test@example.com",
|
||||
"+1234567890",
|
||||
|
|
@ -199,11 +201,11 @@ defmodule SpazioSolazzoWeb.ProfileLiveTest do
|
|||
BookingSystem.create_space(
|
||||
"Test Space #{unique_id}",
|
||||
"test-space-#{unique_id}",
|
||||
"Test description"
|
||||
"Test description",
|
||||
10,
|
||||
12
|
||||
)
|
||||
|
||||
{:ok, asset} = BookingSystem.create_asset("Test Asset", space.id)
|
||||
|
||||
{:ok, time_slot} =
|
||||
BookingSystem.create_time_slot_template(
|
||||
~T[09:00:00],
|
||||
|
|
@ -212,6 +214,6 @@ defmodule SpazioSolazzoWeb.ProfileLiveTest do
|
|||
space.id
|
||||
)
|
||||
|
||||
{space, asset, time_slot}
|
||||
{space, time_slot}
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -26,6 +26,28 @@ defmodule SpazioSolazzo.AuthHelpers do
|
|||
|> AshAuthentication.Phoenix.Plug.store_in_session(user)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Creates a user and logs them into the connection.
|
||||
|
||||
Useful for tests that need an authenticated connection.
|
||||
|
||||
## Parameters
|
||||
|
||||
- `conn` - The test connection
|
||||
- `email` - User's email address
|
||||
- `name` - Optional user's full name (defaults to "Test User")
|
||||
- `phone_number` - Optional phone number (defaults to nil)
|
||||
|
||||
## Examples
|
||||
|
||||
conn = register_and_log_in_user(conn, "test@example.com", "Test User", "+1234567890")
|
||||
conn = register_and_log_in_user(conn, "user@example.com")
|
||||
"""
|
||||
def register_and_log_in_user(conn, email, name \\ "Test User", phone_number \\ nil) do
|
||||
user = register_user(email, name, phone_number)
|
||||
log_in_user(conn, user)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Creates a user via magic link authentication without attaching to a connection.
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue