From 2a5e94b433862af92791636b6229701f4895e246 Mon Sep 17 00:00:00 2001 From: Mia Bennett Date: Thu, 5 Mar 2026 11:36:51 +0930 Subject: [PATCH 1/4] feat(area_management): [PPT-2384] add self["#{level_id}:desk_bookings"] --- drivers/place/area_management.cr | 21 +++++++++++++++++++ drivers/place/desk_bookings_locations.cr | 20 +++++++++++++----- drivers/place/desk_bookings_locations_spec.cr | 14 ++++++++++--- 3 files changed, 47 insertions(+), 8 deletions(-) diff --git a/drivers/place/area_management.cr b/drivers/place/area_management.cr index 160a80d1a81..743be0e8c77 100644 --- a/drivers/place/area_management.cr +++ b/drivers/place/area_management.cr @@ -470,6 +470,27 @@ class Place::AreaManagement < PlaceOS::Driver desk_checked_in: desk_checked_in, } + # Group desk bookings by desk ID for frontend tooltip support + desk_bookings_map = Hash(String, Array(Hash(String, JSON::Any))).new { |h, k| h[k] = [] of Hash(String, JSON::Any) } + locations.each do |loc| + next unless loc["location"]?.try(&.as_s) == "booking" && loc["type"]?.try(&.as_s) == "desk" + desk_id = loc["map_id"]?.try(&.as_s) || loc["asset_id"]?.try(&.as_s) + next unless desk_id + + entry = {} of String => JSON::Any + {"booking_id", "started_at", "ends_at", "duration", "staff_name", "staff_email", "checked_in"}.each do |key| + if val = loc[key]? + entry[key] = val + end + end + desk_bookings_map[desk_id] << entry + end + + # Sort bookings per desk by start time + desk_bookings_map.each_value(&.sort_by! { |b| b["started_at"]?.try(&.as_i64) || 0_i64 }) + + self["#{level_id}:desk_bookings"] = desk_bookings_map + # we need to know the map dimensions to be able to count people in areas map_width = 100.0 map_height = 100.0 diff --git a/drivers/place/desk_bookings_locations.cr b/drivers/place/desk_bookings_locations.cr index 425d14ad067..1b02866d1a4 100644 --- a/drivers/place/desk_bookings_locations.cr +++ b/drivers/place/desk_bookings_locations.cr @@ -44,6 +44,7 @@ class Place::DeskBookingsLocations < PlaceOS::Driver @zone_filter = setting?(Array(String), :zone_filter) || [] of String @zones = nil @building_id = nil + @timezone = nil @map_ids = nil @poll_rate = (setting?(Int32, :poll_rate) || 60).seconds @@ -67,6 +68,11 @@ class Place::DeskBookingsLocations < PlaceOS::Driver getter building_id : String { location_service.building_id.get.as_s } + protected getter timezone : Time::Location do + tz = staff_api.zone(building_id).get["timezone"]?.try(&.as_s?).presence || "UTC" + Time::Location.load(tz) + end + # asset_id => map_id getter map_ids : Hash(String, String) do levels = staff_api.metadata_children(building_id, "desks").get.as_a @@ -96,8 +102,8 @@ class Place::DeskBookingsLocations < PlaceOS::Driver case event.action when "create" - return unless event.in_progress? - # Check if this event is happening now + # Include future bookings for today so "Free until..." can see upcoming bookings + return unless event.booking_end > Time.utc.to_unix logger.debug { "adding new booking" } @bookings[event.user_email] << event when "cancelled", "rejected" @@ -109,9 +115,9 @@ class Place::DeskBookingsLocations < PlaceOS::Driver return unless event.in_progress? @bookings[event.user_email].each { |booking| booking.checked_in = true if booking.id == event.id } when "changed" - # Check if this booking is for today and update as required + # Keep future bookings for today so "Free until..." can see upcoming bookings @bookings[event.user_email].reject! { |booking| booking.id == event.id } - @bookings[event.user_email] << event if event.in_progress? + @bookings[event.user_email] << event if event.booking_end > Time.utc.to_unix else # ignore the update (approve) logger.debug { "booking event was ignored" } @@ -237,10 +243,14 @@ class Place::DeskBookingsLocations < PlaceOS::Driver @known_users : Hash(String, Tuple(String, String)) = Hash(String, Tuple(String, String)).new def query_desk_bookings : Nil + now = Time.local(timezone) + day_start = now.at_beginning_of_day.to_unix + day_end = now.at_end_of_day.to_unix + ids = Set(Int64).new bookings = [] of JSON::Any zones.each do |zone| - bookings.concat staff_api.query_bookings(type: @booking_type, zones: {zone}).get.as_a + bookings.concat staff_api.query_bookings(type: @booking_type, period_start: day_start, period_end: day_end, zones: {zone}).get.as_a rescue error logger.warn(exception: error) { "failed to query bookings in zone: #{zone}" } end diff --git a/drivers/place/desk_bookings_locations_spec.cr b/drivers/place/desk_bookings_locations_spec.cr index 7c83616fd12..7a997acd1d6 100644 --- a/drivers/place/desk_bookings_locations_spec.cr +++ b/drivers/place/desk_bookings_locations_spec.cr @@ -2,8 +2,9 @@ require "placeos-driver/spec" DriverSpecs.mock_driver "Place::DeskBookingsLocations" do system({ - StaffAPI: {StaffAPIMock}, - AreaManagement: {AreaManagementMock}, + StaffAPI: {StaffAPIMock}, + AreaManagement: {AreaManagementMock}, + LocationServices: {LocationServicesMock}, }) settings({ @@ -25,7 +26,7 @@ end # :nodoc: class StaffAPIMock < DriverSpecs::MockDriver - def query_bookings(type : String, zones : Array(String)) + def query_bookings(type : String, zones : Array(String) = [] of String, period_start : Int64? = nil, period_end : Int64? = nil) logger.debug { "Querying desk bookings!" } now = Time.local @@ -82,6 +83,13 @@ class StaffAPIMock < DriverSpecs::MockDriver end end +# :nodoc: +class LocationServicesMock < DriverSpecs::MockDriver + def building_id + "zone-building" + end +end + # :nodoc: class AreaManagementMock < DriverSpecs::MockDriver def update_available(zones : Array(String)) From 2af543f071cf8e832770fbafc23839cc55595a59 Mon Sep 17 00:00:00 2001 From: Mia Bennett Date: Thu, 5 Mar 2026 11:55:20 +0930 Subject: [PATCH 2/4] refactor(desk_bookings_locations): [PPT-2384] fetch bookings for the next 24 hours --- drivers/place/desk_bookings_locations.cr | 14 +++----------- drivers/place/desk_bookings_locations_spec.cr | 12 ++---------- 2 files changed, 5 insertions(+), 21 deletions(-) diff --git a/drivers/place/desk_bookings_locations.cr b/drivers/place/desk_bookings_locations.cr index 1b02866d1a4..ccd9ac0c872 100644 --- a/drivers/place/desk_bookings_locations.cr +++ b/drivers/place/desk_bookings_locations.cr @@ -13,7 +13,7 @@ class Place::DeskBookingsLocations < PlaceOS::Driver accessor area_manager : AreaManagement_1 accessor staff_api : StaffAPI_1 - accessor location_service : LocationServices_1 + accessor location_service : LocationServices_1 # used by `zones` fallback and `map_ids` default_settings({ zone_filter: [] of String, @@ -44,7 +44,6 @@ class Place::DeskBookingsLocations < PlaceOS::Driver @zone_filter = setting?(Array(String), :zone_filter) || [] of String @zones = nil @building_id = nil - @timezone = nil @map_ids = nil @poll_rate = (setting?(Int32, :poll_rate) || 60).seconds @@ -68,11 +67,6 @@ class Place::DeskBookingsLocations < PlaceOS::Driver getter building_id : String { location_service.building_id.get.as_s } - protected getter timezone : Time::Location do - tz = staff_api.zone(building_id).get["timezone"]?.try(&.as_s?).presence || "UTC" - Time::Location.load(tz) - end - # asset_id => map_id getter map_ids : Hash(String, String) do levels = staff_api.metadata_children(building_id, "desks").get.as_a @@ -243,14 +237,12 @@ class Place::DeskBookingsLocations < PlaceOS::Driver @known_users : Hash(String, Tuple(String, String)) = Hash(String, Tuple(String, String)).new def query_desk_bookings : Nil - now = Time.local(timezone) - day_start = now.at_beginning_of_day.to_unix - day_end = now.at_end_of_day.to_unix + period_end = (Time.utc + 24.hours).to_unix ids = Set(Int64).new bookings = [] of JSON::Any zones.each do |zone| - bookings.concat staff_api.query_bookings(type: @booking_type, period_start: day_start, period_end: day_end, zones: {zone}).get.as_a + bookings.concat staff_api.query_bookings(type: @booking_type, period_end: period_end, zones: {zone}).get.as_a rescue error logger.warn(exception: error) { "failed to query bookings in zone: #{zone}" } end diff --git a/drivers/place/desk_bookings_locations_spec.cr b/drivers/place/desk_bookings_locations_spec.cr index 7a997acd1d6..10c8bc43153 100644 --- a/drivers/place/desk_bookings_locations_spec.cr +++ b/drivers/place/desk_bookings_locations_spec.cr @@ -2,9 +2,8 @@ require "placeos-driver/spec" DriverSpecs.mock_driver "Place::DeskBookingsLocations" do system({ - StaffAPI: {StaffAPIMock}, - AreaManagement: {AreaManagementMock}, - LocationServices: {LocationServicesMock}, + StaffAPI: {StaffAPIMock}, + AreaManagement: {AreaManagementMock}, }) settings({ @@ -83,13 +82,6 @@ class StaffAPIMock < DriverSpecs::MockDriver end end -# :nodoc: -class LocationServicesMock < DriverSpecs::MockDriver - def building_id - "zone-building" - end -end - # :nodoc: class AreaManagementMock < DriverSpecs::MockDriver def update_available(zones : Array(String)) From 9116d5856aa46cebb3ee0027204fad3eaa1820cb Mon Sep 17 00:00:00 2001 From: Mia Bennett Date: Thu, 5 Mar 2026 12:00:17 +0930 Subject: [PATCH 3/4] refactor(desk_bookings_locations): [PPT-2384] comments --- drivers/place/desk_bookings_locations.cr | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/drivers/place/desk_bookings_locations.cr b/drivers/place/desk_bookings_locations.cr index ccd9ac0c872..f990abdcc46 100644 --- a/drivers/place/desk_bookings_locations.cr +++ b/drivers/place/desk_bookings_locations.cr @@ -13,7 +13,7 @@ class Place::DeskBookingsLocations < PlaceOS::Driver accessor area_manager : AreaManagement_1 accessor staff_api : StaffAPI_1 - accessor location_service : LocationServices_1 # used by `zones` fallback and `map_ids` + accessor location_service : LocationServices_1 default_settings({ zone_filter: [] of String, From c750ba26f57f2e303e7af5d8d17d3e783948600b Mon Sep 17 00:00:00 2001 From: Mia Bennett Date: Thu, 5 Mar 2026 12:24:19 +0930 Subject: [PATCH 4/4] refactor(are_management): [PPT-2384] ... --- drivers/place/area_management.cr | 5 +++-- drivers/place/desk_bookings_locations.cr | 11 +++++++++++ 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/drivers/place/area_management.cr b/drivers/place/area_management.cr index 743be0e8c77..359ec23f210 100644 --- a/drivers/place/area_management.cr +++ b/drivers/place/area_management.cr @@ -470,9 +470,10 @@ class Place::AreaManagement < PlaceOS::Driver desk_checked_in: desk_checked_in, } - # Group desk bookings by desk ID for frontend tooltip support + # Group upcoming desk bookings by desk ID for frontend tooltip support desk_bookings_map = Hash(String, Array(Hash(String, JSON::Any))).new { |h, k| h[k] = [] of Hash(String, JSON::Any) } - locations.each do |loc| + upcoming = location_service.upcoming_bookings(level_id).get.as_a + upcoming.each do |loc| next unless loc["location"]?.try(&.as_s) == "booking" && loc["type"]?.try(&.as_s) == "desk" desk_id = loc["map_id"]?.try(&.as_s) || loc["asset_id"]?.try(&.as_s) next unless desk_id diff --git a/drivers/place/desk_bookings_locations.cr b/drivers/place/desk_bookings_locations.cr index f990abdcc46..4b23076ab01 100644 --- a/drivers/place/desk_bookings_locations.cr +++ b/drivers/place/desk_bookings_locations.cr @@ -155,6 +155,17 @@ class Place::DeskBookingsLocations < PlaceOS::Driver logger.debug { "searching devices in zone #{zone_id}" } return [] of Nil if location && location != "booking" + bookings = [] of Booking + @bookings.each_value(&.each { |booking| + next unless zone_id.in?(booking.zones) + next unless booking.in_progress? + bookings << booking + }) + map_bookings(bookings) + end + + def upcoming_bookings(zone_id : String) + logger.debug { "fetching upcoming bookings in zone #{zone_id}" } bookings = [] of Booking @bookings.each_value(&.each { |booking| next unless zone_id.in?(booking.zones)